diff --git a/.gitignore b/.gitignore index 6f6dd6136c..9039e8ec4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.toDelete output/** *.class *~ diff --git a/application/.gitignore b/application/.gitignore index 08eb0a07a6..b5246c6bdd 100644 --- a/application/.gitignore +++ b/application/.gitignore @@ -1 +1,2 @@ -!bin/ \ No newline at end of file +!bin/ +/bin/ 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/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index d4c42d82b7..0c73470d5c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -51,13 +51,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody; import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg; import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; @@ -205,25 +199,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) { refreshAttributes(msg); - Set keys = msg.getDeletedKeys(); if (attributeSubscriptions.size() > 0) { ToDeviceMsg notification = null; if (msg.isDeleted()) { - List sharedKeys = keys.stream() + List sharedKeys = msg.getDeletedKeys().stream() .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope())) .collect(Collectors.toList()); notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys)); } else { - List attributes = keys.stream() - .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope())) - .map(key -> deviceAttributes.getServerPublicAttribute(key.getAttributeKey())) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - if (attributes.size() > 0) { - notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes)); - } else { - logger.debug("[{}] No public server side attributes changed!", deviceId); + if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) { + List attributes = new ArrayList<>(msg.getValues()); + if (attributes.size() > 0) { + notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes)); + } else { + logger.debug("[{}] No public server side attributes changed!", deviceId); + } } } if (notification != null) { 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 0b9e37c4e7..33dfa3f007 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 @@ -24,17 +24,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.id.CustomerId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.PluginId; +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.TenantId; 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.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; @@ -89,103 +91,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<>(); @@ -268,24 +274,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); } @@ -295,8 +378,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/config/MvcCorsProperties.java b/application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java new file mode 100644 index 0000000000..62b4ec266f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.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.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by yyh on 2017/5/2. + * CORS configuration + */ +@Configuration +@ConfigurationProperties(prefix = "spring.mvc.cors") +public class MvcCorsProperties { + + private Map mappings = new HashMap<>(); + + public MvcCorsProperties() { + } + + public Map getMappings() { + return mappings; + } + + public void setMappings(Map mappings) { + this.mappings = mappings; + } +} diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index a7918e05b7..cdca099c3a 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -18,7 +18,9 @@ package org.thingsboard.server.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -34,11 +36,15 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.*; import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; +import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; import java.util.ArrayList; import java.util.Arrays; @@ -56,6 +62,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt public static final String WEBJARS_ENTRY_POINT = "/webjars/**"; public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; + public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; @@ -87,10 +94,18 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt return filter; } + @Bean + protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception { + RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper); + filter.setAuthenticationManager(this.authenticationManager); + return filter; + } + @Bean protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { List pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); - pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT)); + pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, + PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT)); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); @@ -135,6 +150,8 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @Override protected void configure(HttpSecurity http) throws Exception { http.headers().cacheControl().disable().frameOptions().disable() + .and() + .cors() .and() .csrf().disable() .exceptionHandling() @@ -146,6 +163,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point + .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points .and() @@ -156,8 +174,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler) .and() .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); } + + + @Bean + @ConditionalOnMissingBean(CorsFilter.class) + public CorsFilter corsFilter(@Autowired MvcCorsProperties mvcCorsProperties) { + if (mvcCorsProperties.getMappings().size() == 0) { + return new CorsFilter(new UrlBasedCorsConfigurationSource()); + } else { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.setCorsConfigurations(mvcCorsProperties.getMappings()); + return new CorsFilter(source); + } + } } 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/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 5ef52c236d..d06f2bed9d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -36,6 +36,7 @@ import org.thingsboard.server.exception.ThingsboardException; import org.thingsboard.server.service.mail.MailService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtToken; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -167,7 +168,8 @@ public class AuthController extends BaseController { String encodedPassword = passwordEncoder.encode(password); UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword); User user = userService.findUserById(credentials.getUserId()); - SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); String baseUrl = constructBaseUrl(request); String loginUrl = String.format("%s/login", baseUrl); String email = user.getEmail(); @@ -201,7 +203,8 @@ public class AuthController extends BaseController { userCredentials.setResetToken(null); userCredentials = userService.saveUserCredentials(userCredentials); User user = userService.findUserById(userCredentials.getUserId()); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal); String baseUrl = constructBaseUrl(request); String loginUrl = String.format("%s/login", baseUrl); String email = user.getEmail(); 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 034206f73e..12d43e00a4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -22,10 +22,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; @@ -36,6 +34,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; @@ -44,6 +43,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; @@ -78,6 +78,9 @@ public abstract class BaseController { @Autowired protected DeviceService deviceService; + @Autowired + protected AssetService assetService; + @Autowired protected DeviceCredentialsService deviceCredentialsService; @@ -102,6 +105,9 @@ public abstract class BaseController { @Autowired protected ActorService actorService; + @Autowired + protected RelationService relationService; + @ExceptionHandler(ThingsboardException.class) public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { @@ -251,6 +257,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); @@ -270,6 +313,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); @@ -385,6 +447,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(); @@ -410,7 +482,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/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 96458cf273..091eb0edce 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -42,6 +45,28 @@ public class CustomerController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET) + @ResponseBody + public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId); + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode infoObject = objectMapper.createObjectNode(); + infoObject.put("title", customer.getTitle()); + boolean isPublic = false; + if (customer.getAdditionalInfo() != null && customer.getAdditionalInfo().has("isPublic")) { + isPublic = customer.getAdditionalInfo().get("isPublic").asBoolean(); + } + infoObject.put("isPublic", isPublic); + return infoObject; + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/title", method = RequestMethod.GET, produces = "application/text") @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index d72f02548e..3812610d20 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -18,6 +18,7 @@ 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.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.CustomerId; @@ -116,6 +117,21 @@ public class DashboardController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST) + @ResponseBody + public Dashboard assignDashboardToPublicCustomer(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException { + checkParameter("dashboardId", strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId()); + return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId())); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 3df1baef36..38efcb82d5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -19,6 +19,7 @@ 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.Device; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; @@ -114,6 +115,21 @@ public class DeviceController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/device/{deviceId}", method = RequestMethod.POST) + @ResponseBody + public Device assignDeviceToPublicCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException { + checkParameter("deviceId", strDeviceId); + try { + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + Device device = checkDeviceId(deviceId); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId()); + return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId())); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET) @ResponseBody 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/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 5ba84bf112..811f39f5d1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -16,32 +16,40 @@ package org.thingsboard.server.service.security.auth.jwt; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.InsufficientAuthenticationException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; +import java.util.UUID; + @Component public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { private final JwtTokenFactory tokenFactory; private final UserService userService; + private final CustomerService customerService; @Autowired - public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) { + public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) { this.userService = userService; + this.customerService = customerService; this.tokenFactory = tokenFactory; } @@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide Assert.notNull(authentication, "No authentication data provided"); RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken); + UserPrincipal principal = unsafeUser.getUserPrincipal(); + SecurityUser securityUser; + if (principal.getType() == UserPrincipal.Type.USER_NAME) { + securityUser = authenticateByUserId(unsafeUser.getId()); + } else { + securityUser = authenticateByPublicId(principal.getValue()); + } + return new RefreshAuthenticationToken(securityUser); + } - User user = userService.findUserById(unsafeUser.getId()); + private SecurityUser authenticateByUserId(UserId userId) { + User user = userService.findUserById(userId); if (user == null) { throw new UsernameNotFoundException("User not found by refresh token"); } @@ -67,9 +85,44 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - return new RefreshAuthenticationToken(securityUser); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + + return securityUser; + } + + private SecurityUser authenticateByPublicId(String publicId) { + CustomerId customerId; + try { + customerId = new CustomerId(UUID.fromString(publicId)); + } catch (Exception e) { + throw new BadCredentialsException("Refresh token is not valid"); + } + Customer publicCustomer = customerService.findCustomerById(customerId); + if (publicCustomer == null) { + throw new UsernameNotFoundException("Public entity not found by refresh token"); + } + boolean isPublic = false; + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) { + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean(); + } + if (!isPublic) { + throw new BadCredentialsException("Refresh token is not valid"); + } + User user = new User(new UserId(UUIDBased.EMPTY)); + user.setTenantId(publicCustomer.getTenantId()); + user.setCustomerId(publicCustomer.getId()); + user.setEmail(publicId); + user.setAuthority(Authority.CUSTOMER_USER); + user.setFirstName("Public"); + user.setLastName("Public"); + + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId); + + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal); + + return securityUser; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java new file mode 100644 index 0000000000..54ef093804 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.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.service.security.auth.rest; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PublicLoginRequest { + + private String publicId; + + @JsonCreator + public PublicLoginRequest(@JsonProperty("publicId") String publicId) { + this.publicId = publicId; + } + + public String getPublicId() { + return publicId; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index 686bb465c4..af10674294 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -23,20 +23,31 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; + +import java.util.UUID; @Component public class RestAuthenticationProvider implements AuthenticationProvider { private final BCryptPasswordEncoder encoder; private final UserService userService; + private final CustomerService customerService; @Autowired - public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) { + public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) { this.userService = userService; + this.customerService = customerService; this.encoder = encoder; } @@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider { public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.notNull(authentication, "No authentication data provided"); - String username = (String) authentication.getPrincipal(); - String password = (String) authentication.getCredentials(); + Object principal = authentication.getPrincipal(); + if (!(principal instanceof UserPrincipal)) { + throw new BadCredentialsException("Authentication Failed. Bad user principal."); + } + UserPrincipal userPrincipal = (UserPrincipal) principal; + if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { + String username = userPrincipal.getValue(); + String password = (String) authentication.getCredentials(); + return authenticateByUsernameAndPassword(userPrincipal, username, password); + } else { + String publicId = userPrincipal.getValue(); + return authenticateByPublicId(userPrincipal, publicId); + } + } + + private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) { User user = userService.findUserByEmail(username); if (user == null) { throw new UsernameNotFoundException("User not found: " + username); @@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider { if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); + } + + private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) { + CustomerId customerId; + try { + customerId = new CustomerId(UUID.fromString(publicId)); + } catch (Exception e) { + throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); + } + Customer publicCustomer = customerService.findCustomerById(customerId); + if (publicCustomer == null) { + throw new UsernameNotFoundException("Public entity not found: " + publicId); + } + boolean isPublic = false; + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) { + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean(); + } + if (!isPublic) { + throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); + } + User user = new User(new UserId(UUIDBased.EMPTY)); + user.setTenantId(publicCustomer.getTenantId()); + user.setCustomerId(publicCustomer.getId()); + user.setEmail(publicId); + user.setAuthority(Authority.CUSTOMER_USER); + user.setFirstName("Public"); + user.setLastName("Public"); + + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal); return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java index d191905056..c32b1f221e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java @@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; +import org.thingsboard.server.service.security.model.UserPrincipal; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF throw new AuthenticationServiceException("Username or Password not provided"); } - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword()); return this.getAuthenticationManager().authenticate(token); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java new file mode 100644 index 0000000000..3a3b7cb2f1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java @@ -0,0 +1,96 @@ +/** + * 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.service.security.auth.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; +import org.thingsboard.server.service.security.model.UserPrincipal; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { + private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class); + + private final AuthenticationSuccessHandler successHandler; + private final AuthenticationFailureHandler failureHandler; + + private final ObjectMapper objectMapper; + + public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, + AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { + super(defaultProcessUrl); + this.successHandler = successHandler; + this.failureHandler = failureHandler; + this.objectMapper = mapper; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + if (!HttpMethod.POST.name().equals(request.getMethod())) { + if(logger.isDebugEnabled()) { + logger.debug("Authentication method not supported. Request method: " + request.getMethod()); + } + throw new AuthMethodNotSupportedException("Authentication method not supported"); + } + + PublicLoginRequest loginRequest; + try { + loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class); + } catch (Exception e) { + throw new AuthenticationServiceException("Invalid public login request payload"); + } + + if (StringUtils.isBlank(loginRequest.getPublicId())) { + throw new AuthenticationServiceException("Public Id is not provided"); + } + + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, ""); + + return this.getAuthenticationManager().authenticate(token); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, + Authentication authResult) throws IOException, ServletException { + successHandler.onAuthenticationSuccess(request, response, authResult); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + SecurityContextHolder.clearContext(); + failureHandler.onAuthenticationFailure(request, response, failed); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index 2da3a97366..0839695261 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java @@ -30,6 +30,7 @@ public class SecurityUser extends User { private Collection authorities; private boolean enabled; + private UserPrincipal userPrincipal; public SecurityUser() { super(); @@ -39,9 +40,10 @@ public class SecurityUser extends User { super(id); } - public SecurityUser(User user, boolean enabled) { + public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) { super(user); this.enabled = enabled; + this.userPrincipal = userPrincipal; } public Collection getAuthorities() { @@ -57,8 +59,16 @@ public class SecurityUser extends User { return enabled; } + public UserPrincipal getUserPrincipal() { + return userPrincipal; + } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public void setUserPrincipal(UserPrincipal userPrincipal) { + this.userPrincipal = userPrincipal; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java new file mode 100644 index 0000000000..6f23783c0b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.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.service.security.model; + +public class UserPrincipal { + + private final Type type; + private final String value; + + public UserPrincipal(Type type, String value) { + this.type = type; + this.value = value; + } + + public Type getType() { + return type; + } + + public String getValue() { + return value; + } + + public enum Type { + USER_NAME, + PUBLIC_ID + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 8ade253412..20de6d8e73 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import java.util.Arrays; import java.util.List; @@ -43,6 +44,7 @@ public class JwtTokenFactory { private static final String FIRST_NAME = "firstName"; private static final String LAST_NAME = "lastName"; private static final String ENABLED = "enabled"; + private static final String IS_PUBLIC = "isPublic"; private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; @@ -63,12 +65,15 @@ public class JwtTokenFactory { if (securityUser.getAuthority() == null) throw new IllegalArgumentException("User doesn't have any privileges"); - Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); + UserPrincipal principal = securityUser.getUserPrincipal(); + String subject = principal.getValue(); + Claims claims = Jwts.claims().setSubject(subject); claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList())); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(FIRST_NAME, securityUser.getFirstName()); claims.put(LAST_NAME, securityUser.getLastName()); claims.put(ENABLED, securityUser.isEnabled()); + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); if (securityUser.getTenantId() != null) { claims.put(TENANT_ID, securityUser.getTenantId().getId().toString()); } @@ -104,6 +109,9 @@ public class JwtTokenFactory { securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); securityUser.setLastName(claims.get(LAST_NAME, String.class)); securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); + securityUser.setUserPrincipal(principal); String tenantId = claims.get(TENANT_ID, String.class); if (tenantId != null) { securityUser.setTenantId(new TenantId(UUID.fromString(tenantId))); @@ -123,9 +131,11 @@ public class JwtTokenFactory { DateTime currentTime = new DateTime(); - Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); + UserPrincipal principal = securityUser.getUserPrincipal(); + Claims claims = Jwts.claims().setSubject(principal.getValue()); claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name())); claims.put(USER_ID, securityUser.getId().getId().toString()); + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); String token = Jwts.builder() .setClaims(claims) @@ -150,8 +160,10 @@ public class JwtTokenFactory { if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) { throw new IllegalArgumentException("Invalid Refresh Token scope"); } + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); - securityUser.setEmail(subject); + securityUser.setUserPrincipal(principal); return securityUser; } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index c62672595c..3bacbfa60f 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}" @@ -189,11 +189,33 @@ cache: updates: # Enable/disable updates checking. enabled: "${UPDATES_ENABLED:true}" - + + # spring CORS configuration +spring.mvc.cors: + mappings: + # Intercept path + "/api/auth/**": + #Comma-separated list of origins to allow. '*' allows all origins. When not set,CORS support is disabled. + allowed-origins: "*" + #Comma-separated list of methods to allow. '*' allows all methods. + allowed-methods: "POST,GET,OPTIONS" + #Comma-separated list of headers to allow in a request. '*' allows all headers. + allowed-headers: "*" + #How long, in seconds, the response from a pre-flight request can be cached by clients. + max-age: "1800" + #Set whether credentials are supported. When not set, credentials are not supported. + allow-credentials: "true" + "/api/v1/**": + allowed-origins: "*" + allowed-methods: "*" + allowed-headers: "*" + max-age: "1800" + allow-credentials: "true" + # SQL DAO Configuration sql: enabled: "${SQL_ENABLED:false}" datasource: url: "${SQL_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}" username: "${SQL_DATASOURCE_USERNAME:postgres}" - password: "${SQL_DATASOURCE_PASSWORD:postgres}" + password: "${SQL_DATASOURCE_PASSWORD:postgres}" 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/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java index 896c549d08..b1076e0413 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java @@ -288,7 +288,7 @@ public class CustomerControllerTest extends AbstractControllerTest { for (int i=0;i<143;i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); String title = title1+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); @@ -299,7 +299,7 @@ public class CustomerControllerTest extends AbstractControllerTest { for (int i=0;i<175;i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); String title = title2+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index ca4abfeab4..5d72076ee6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -149,7 +149,7 @@ public class TenantControllerTest extends AbstractControllerTest { List tenantsTitle1 = new ArrayList<>(); for (int i=0;i<134;i++) { Tenant tenant = new Tenant(); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); String title = title1+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); tenant.setTitle(title); @@ -159,7 +159,7 @@ public class TenantControllerTest extends AbstractControllerTest { List tenantsTitle2 = new ArrayList<>(); for (int i=0;i<127;i++) { Tenant tenant = new Tenant(); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); String title = title2+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); tenant.setTitle(title); 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..669167000f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -0,0 +1,55 @@ +/** + * 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.AllArgsConstructor; +import lombok.Builder; +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 +@Builder +@AllArgsConstructor +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/CassandraAbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java index 99c82a0cf6..b35ed37cf2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java @@ -187,4 +187,14 @@ public abstract class CassandraAbstractModelDao, D> exte List entities = findListByStatement(QueryBuilder.select().all().from(getColumnFamilyName()).setConsistencyLevel(cluster.getDefaultReadConsistencyLevel())); return DaoUtil.convertDataList(entities); } + + protected static Function, T> toDataFunction() { + return new Function, T>() { + @Nullable + @Override + public T apply(@Nullable BaseEntity entity) { + return entity != null ? entity.toData() : null; + } + }; + } } 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..8f32b4c987 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -0,0 +1,38 @@ +/** + * 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; + +import java.util.UUID; + +/** + * Created by ashvayka on 11.05.17. + */ +public interface AlarmDao extends Dao { + + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + + ListenableFuture findAlarmByIdAsync(UUID key); + + Alarm 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..dcb01ba199 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDaoImpl.java @@ -0,0 +1,77 @@ +/** + * 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.datastax.driver.core.querybuilder.QueryBuilder; +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.alarm.Alarm; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.CassandraAbstractModelDao; +import org.thingsboard.server.dao.model.AlarmEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.UUID; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static org.thingsboard.server.dao.model.ModelConstants.*; + +@Component +@Slf4j +public class AlarmDaoImpl extends CassandraAbstractModelDao implements AlarmDao { + + @Override + protected Class getColumnFamilyClass() { + return AlarmEntity.class; + } + + @Override + protected String getColumnFamilyName() { + return ALARM_COLUMN_FAMILY_NAME; + } + + @Override + public Alarm save(Alarm alarm) { + log.debug("Save asset [{}] ", alarm); + return super.save(alarm); + } + + @Override + public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + Select select = select().from(ALARM_COLUMN_FAMILY_NAME); + Select.Where query = select.where(); + query.and(eq(ALARM_TENANT_ID_PROPERTY, tenantId.getId())); + query.and(eq(ALARM_ORIGINATOR_ID_PROPERTY, originator.getId())); + query.and(eq(ALARM_ORIGINATOR_TYPE_PROPERTY, originator.getEntityType())); + query.and(eq(ALARM_TYPE_PROPERTY, type)); + query.limit(1); + query.orderBy(QueryBuilder.asc(ModelConstants.ALARM_TYPE_PROPERTY), QueryBuilder.desc(ModelConstants.ID_PROPERTY)); + return findOneByStatementAsync(query); + } + + @Override + public ListenableFuture findAlarmByIdAsync(UUID key) { + log.debug("Get alarm by id {}", key); + Select.Where query = select().from(ALARM_BY_ID_VIEW_NAME).where(eq(ModelConstants.ID_PROPERTY, key)); + query.limit(1); + log.trace("Execute query {}", query); + return findOneByStatementAsync(query); + } +} 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..6508db21a1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.Device; +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.DeviceId; +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 findAlarmById(AlarmId alarmId); + + 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..6d484ba07c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -0,0 +1,252 @@ +/** + * 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.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.Tenant; +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.id.EntityId; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.dao.entity.BaseEntityService; +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.RelationService; +import org.thingsboard.server.dao.relation.RelationsSearchParameters; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantDao; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@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()); + } + if (alarm.getEndTs() == 0L) { + alarm.setEndTs(alarm.getStartTs()); + } + Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get(); + if (existing == null || existing.getStatus().isCleared()) { + log.debug("New Alarm : {}", alarm); + Alarm saved = alarmDao.save(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 alarmDao.save(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 Alarm alarm) { + if (alarm == null) { + return false; + } else { + AlarmStatus oldStatus = alarm.getStatus(); + AlarmStatus newStatus = update.getStatus(); + alarmDao.save(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 Alarm alarm) { + 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(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 Alarm alarm) { + 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(alarm); + updateRelations(alarm, oldStatus, newStatus); + return true; + } + } + }); + } + + @Override + public ListenableFuture findAlarmById(AlarmId alarmId) { + log.trace("Executing findAlarmById [{}]", alarmId); + validateId(alarmId, "Incorrect alarmId " + alarmId); + return alarmDao.findAlarmByIdAsync(alarmId.getId()); + } + + @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.findAlarmByIdAsync(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 { + Tenant 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..220cbcb1ec --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -0,0 +1,87 @@ +/** + * 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.page.TextPageLink; +import org.thingsboard.server.dao.Dao; + +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 + */ + Asset 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..2f2cf13665 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java @@ -0,0 +1,106 @@ +/** + * 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.CassandraAbstractSearchTextDao; +import org.thingsboard.server.dao.DaoUtil; +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 CassandraAbstractSearchTextDao implements AssetDao { + + @Override + protected Class getColumnFamilyClass() { + return AssetEntity.class; + } + + @Override + protected String getColumnFamilyName() { + return ASSET_COLUMN_FAMILY_NAME; + } + + @Override + public Asset save(Asset asset) { + log.debug("Save asset [{}] ", asset); + return save(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 DaoUtil.convertDataList(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 DaoUtil.convertDataList(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)); + AssetEntity assetEntity = (AssetEntity) findOneByStatement(query); + return Optional.ofNullable(DaoUtil.getData(assetEntity)); + } + +} 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..addf17bd39 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -0,0 +1,282 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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.Customer; +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.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.nosql.CustomerEntity; +import org.thingsboard.server.dao.model.nosql.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); + return assetDao.findById(assetId.getId()); + } + + @Override + public ListenableFuture findAssetByIdAsync(AssetId assetId) { + log.trace("Executing findAssetById [{}]", assetId); + validateId(assetId, "Incorrect assetId " + assetId); + return assetDao.findByIdAsync(assetId.getId()); + } + + @Override + public Optional findAssetByTenantIdAndName(TenantId tenantId, String name) { + log.trace("Executing findAssetByTenantIdAndName [{}][{}]", tenantId, name); + validateId(tenantId, "Incorrect tenantId " + tenantId); + return assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name); + } + + @Override + public Asset saveAsset(Asset asset) { + log.trace("Executing saveAsset [{}]", asset); + assetValidator.validate(asset); + return 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 assets = assetDao.findAssetsByTenantId(tenantId.getId(), pageLink); + 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); + return assetDao.findAssetsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(assetIds)); + } + + @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 assets = assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + 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); + return assetDao.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId.getId(), customerId.getId(), toUUIDs(assetIds)); + } + + @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 { + Tenant 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)) { + Customer 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(Asset entity) { + deleteAsset(new AssetId(entity.getId().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(Asset entity) { + unassignAssetFromCustomer(new AssetId(entity.getId().getId())); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java index 1bb682bdd7..92ca9c4e17 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java @@ -15,6 +15,13 @@ */ package org.thingsboard.server.dao.customer; +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY; + +import java.util.Optional; +import com.datastax.driver.core.querybuilder.Select; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; @@ -53,4 +60,15 @@ public class CassandraCustomerDao extends CassandraAbstractSearchTextDao findCustomersByTenantIdAndTitle(UUID tenantId, String title) { + Select select = select().from(CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME); + Select.Where query = select.where(); + query.and(eq(CUSTOMER_TENANT_ID_PROPERTY, tenantId)); + query.and(eq(CUSTOMER_TITLE_PROPERTY, title)); + CustomerEntity customerEntity = findOneByStatement(query); + Customer customer = DaoUtil.getData(customerEntity); + return Optional.ofNullable(customer); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java index e26fc986bf..3c8f66da0a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.customer; +import java.util.Optional; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.dao.Dao; @@ -43,5 +44,14 @@ public interface CustomerDao extends Dao { * @return the list of customer objects */ List findCustomersByTenantId(UUID tenantId, TextPageLink pageLink); + + /** + * Find customers by tenantId and customer title. + * + * @param tenantId the tenantId + * @param title the customer title + * @return the optional customer object + */ + Optional findCustomersByTenantIdAndTitle(UUID tenantId, String title); } 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 566d71818e..4600d9f048 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; @@ -24,13 +25,17 @@ import org.thingsboard.server.common.data.page.TextPageLink; public interface CustomerService { Customer findCustomerById(CustomerId customerId); - + + ListenableFuture findCustomerByIdAsync(CustomerId customerId); + Customer saveCustomer(Customer customer); void deleteCustomer(CustomerId customerId); - + + Customer findOrCreatePublicCustomer(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 3e9e472c06..ba5c9a9e79 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 @@ -15,30 +15,42 @@ */ package org.thingsboard.server.dao.customer; +import static org.thingsboard.server.dao.service.Validator.validateId; +import java.io.IOException; +import java.util.List; +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.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Tenant; +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.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.user.UserService; - -import java.util.List; @Service @Slf4j -public class CustomerServiceImpl implements CustomerService { +public class CustomerServiceImpl extends BaseEntityService implements CustomerService { + + private static final String PUBLIC_CUSTOMER_TITLE = "Public"; @Autowired private CustomerDao customerDao; @@ -62,6 +74,13 @@ public class CustomerServiceImpl implements CustomerService { return customerDao.findById(customerId.getId()); } + @Override + public ListenableFuture findCustomerByIdAsync(CustomerId customerId) { + log.trace("Executing findCustomerByIdAsync [{}]", customerId); + validateId(customerId, "Incorrect customerId " + customerId); + return customerDao.findByIdAsync(customerId.getId()); + } + @Override public Customer saveCustomer(Customer customer) { log.trace("Executing saveCustomer [{}]", customer); @@ -72,24 +91,45 @@ public class CustomerServiceImpl implements CustomerService { @Override public void deleteCustomer(CustomerId customerId) { log.trace("Executing deleteCustomer [{}]", customerId); - Validator.validateId(customerId, "Incorrect tenantId " + customerId); + Validator.validateId(customerId, "Incorrect customerId " + customerId); Customer customer = findCustomerById(customerId); if (customer == null) { throw new IncorrectParameterException("Unable to delete non-existent customer."); } 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()); } + @Override + public Customer findOrCreatePublicCustomer(TenantId tenantId) { + log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId); + Validator.validateId(tenantId, "Incorrect customerId " + tenantId); + Optional publicCustomerOpt = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE); + if (publicCustomerOpt.isPresent()) { + return publicCustomerOpt.get(); + } else { + Customer publicCustomer = new Customer(); + publicCustomer.setTenantId(tenantId); + publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE); + try { + publicCustomer.setAdditionalInfo(new ObjectMapper().readValue("{ \"isPublic\": true }", JsonNode.class)); + } catch (IOException e) { + throw new IncorrectParameterException("Unable to create public customer.", e); + } + return customerDao.save(publicCustomer); + } + } + @Override public TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) { log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink); List customers = customerDao.findCustomersByTenantId(tenantId.getId(), pageLink); - return new TextPageData(customers, pageLink); + return new TextPageData<>(customers, pageLink); } @Override @@ -101,11 +141,35 @@ public class CustomerServiceImpl implements CustomerService { private DataValidator customerValidator = new DataValidator() { + + @Override + protected void validateCreate(Customer customer) { + customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent( + c -> { + throw new DataValidationException("Customer with such title already exists!"); + } + ); + } + + @Override + protected void validateUpdate(Customer customer) { + customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent( + c -> { + if (!c.getId().equals(customer.getUuidId())) { + throw new DataValidationException("Customer with such title already exists!"); + } + } + ); + } + @Override protected void validateDataImpl(Customer customer) { if (StringUtils.isEmpty(customer.getTitle())) { throw new DataValidationException("Customer title should be specified!"); } + if (customer.getTitle().equals(PUBLIC_CUSTOMER_TITLE)) { + throw new DataValidationException("'Public' title for customer is system reserved!"); + } if (!StringUtils.isEmpty(customer.getEmail())) { validateEmail(customer.getEmail()); } 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 fca73c4ae4..bdca53648b 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 @@ -29,6 +29,7 @@ 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.ModelConstants; import org.thingsboard.server.dao.service.DataValidator; @@ -40,7 +41,7 @@ import java.util.List; @Service @Slf4j -public class DashboardServiceImpl implements DashboardService { +public class DashboardServiceImpl extends BaseEntityService implements DashboardService { @Autowired private DashboardDao dashboardDao; @@ -86,6 +87,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 a5707ce29d..819f3ae18c 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.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -48,7 +49,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; @@ -125,6 +126,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..62cd08a218 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/AlarmEntity.java @@ -0,0 +1,236 @@ +/** + * 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; + } + + public UUID getOriginatorId() { + return originatorId; + } + + public void setOriginatorId(UUID originatorId) { + this.originatorId = originatorId; + } + + public EntityType getOriginatorType() { + return originatorType; + } + + public void setOriginatorType(EntityType originatorType) { + this.originatorType = originatorType; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public AlarmSeverity getSeverity() { + return severity; + } + + public void setSeverity(AlarmSeverity severity) { + this.severity = severity; + } + + public AlarmStatus getStatus() { + return status; + } + + public void setStatus(AlarmStatus status) { + this.status = status; + } + + public Long getStartTs() { + return startTs; + } + + public void setStartTs(Long startTs) { + this.startTs = startTs; + } + + public Long getEndTs() { + return endTs; + } + + public void setEndTs(Long endTs) { + this.endTs = endTs; + } + + public Long getAckTs() { + return ackTs; + } + + public void setAckTs(Long ackTs) { + this.ackTs = ackTs; + } + + public Long getClearTs() { + return clearTs; + } + + public void setClearTs(Long clearTs) { + this.clearTs = clearTs; + } + + public JsonNode getDetails() { + return details; + } + + public void setDetails(JsonNode details) { + this.details = details; + } + + public Boolean getPropagate() { + return propagate; + } + + public void setPropagate(Boolean propagate) { + this.propagate = propagate; + } + + @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/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index b721780343..ff2a37a1a4 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 @@ -112,6 +112,7 @@ public class ModelConstants { public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text"; + public static final String CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME = "customer_by_tenant_and_title"; /** * Cassandra device constants. @@ -126,6 +127,51 @@ 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 = TENANT_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 = TENANT_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"; + + public static final String ALARM_BY_ID_VIEW_NAME = "alarm_by_id"; + + /** + * 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/nosql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java index acd5b8df8f..8224fb83d0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java @@ -99,23 +99,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/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 b9b73c6c89..578484018f 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,11 +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.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,6 +32,7 @@ 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; @@ -43,9 +47,10 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; +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); @@ -102,6 +107,12 @@ public class BasePluginService implements PluginService { return pluginDao.findById(pluginId); } + @Override + public ListenableFuture findPluginByIdAsync(PluginId pluginId) { + validateId(pluginId, "Incorrect plugin id for search plugin request."); + return pluginDao.findByIdAsync(pluginId.getId()); + } + @Override public PluginMetaData findPluginByApiToken(String apiToken) { Validator.validateString(apiToken, "Incorrect plugin apiToken for search request."); @@ -196,6 +207,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..d3a886beec --- /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.CassandraAbstractAsyncDao; +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 CassandraAbstractAsyncDao 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 87e35ed3e1..1bc0d92285 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.plugin.PluginService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -50,7 +55,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); @@ -163,6 +168,12 @@ public class BaseRuleService implements RuleService { return ruleDao.findById(ruleId.getId()); } + @Override + public ListenableFuture findRuleByIdAsync(RuleId ruleId) { + validateId(ruleId, "Incorrect rule id for search rule request."); + return ruleDao.findByIdAsync(ruleId.getId()); + } + @Override public List findPluginRules(String pluginToken) { return ruleDao.findRulesByPlugin(pluginToken); @@ -228,6 +239,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/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java index 3dfaccdec8..9b6c4d1341 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java @@ -28,6 +28,7 @@ import org.thingsboard.server.dao.model.sql.CustomerEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import java.util.List; +import java.util.Optional; import java.util.UUID; /** @@ -59,4 +60,10 @@ public class JpaCustomerDao extends JpaAbstractSearchTextDao findCustomersByTenantIdAndTitle(UUID tenantId, String title) { + // TODO: vsosliuk implement + return null; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java index 7067a55839..b0d9a712e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java @@ -35,5 +35,4 @@ public interface EventRepository extends CrudRepository, JpaS EventEntity findByTenantIdAndEntityTypeAndEntityIdAndEventTypeAndEventUid( UUID tenantId, EntityType entityType, UUID id, String eventType, String eventUid); - } 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 13b5090bc3..21c8001350 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; @@ -23,6 +24,8 @@ import org.thingsboard.server.common.data.page.TextPageLink; public interface TenantService { Tenant findTenantById(TenantId tenantId); + + ListenableFuture findTenantByIdAsync(TenantId customerId); Tenant saveTenant(Tenant tenant); @@ -31,5 +34,4 @@ public interface TenantService { TextPageData findTenants(TextPageLink pageLink); 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 976cb71748..e80af2fff7 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.tenant; +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; @@ -26,6 +27,7 @@ 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.plugin.PluginService; import org.thingsboard.server.dao.rule.RuleService; @@ -37,21 +39,23 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import java.util.List; +import static org.thingsboard.server.dao.service.Validator.validateId; + @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; @@ -66,7 +70,7 @@ public class TenantServiceImpl implements TenantService { @Autowired private PluginService pluginService; - + @Override public Tenant findTenantById(TenantId tenantId) { log.trace("Executing findTenantById [{}]", tenantId); @@ -74,6 +78,13 @@ public class TenantServiceImpl implements TenantService { return tenantDao.findById(tenantId.getId()); } + @Override + public ListenableFuture findTenantByIdAsync(TenantId tenantId) { + log.trace("Executing TenantIdAsync [{}]", tenantId); + validateId(tenantId, "Incorrect tenantId " + tenantId); + return tenantDao.findByIdAsync(tenantId.getId()); + } + @Override public Tenant saveTenant(Tenant tenant) { log.trace("Executing saveTenant [{}]", tenant); @@ -94,6 +105,7 @@ public class TenantServiceImpl implements TenantService { ruleService.deleteRulesByTenantId(tenantId); pluginService.deletePluginsByTenantId(tenantId); tenantDao.removeById(tenantId.getId()); + deleteEntityRelations(tenantId); } @Override @@ -122,10 +134,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/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 7a59dcc51b..033fa5f1e2 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 @@ -24,7 +24,7 @@ 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.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -49,30 +49,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"); } @@ -80,13 +80,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) { @@ -94,7 +94,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); } @@ -109,15 +109,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/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 738c5aa63c..d6c7443944 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.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.CassandraAbstractAsyncDao; @@ -93,8 +94,8 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } @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 @@ -107,9 +108,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } - 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(); @@ -118,7 +119,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem 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); @@ -132,11 +133,11 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } } - 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); @@ -144,13 +145,13 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem 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); @@ -186,19 +187,19 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } } - 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); } @@ -208,21 +209,21 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem .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); @@ -234,30 +235,30 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } @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()); @@ -266,11 +267,11 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } @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); @@ -278,11 +279,11 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } @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)); } @@ -338,9 +339,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem * 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/TimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java index 452eb2cae3..b564cca08a 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; @@ -31,17 +32,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 20731b22c0..9c4a224ab6 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 @@ -19,6 +19,7 @@ import com.datastax.driver.core.ResultSet; 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.id.UUIDBased; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvQuery; @@ -31,15 +32,15 @@ import java.util.List; */ 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 7f4d6c07fb..709e62da13 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 @@ -31,6 +31,7 @@ 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.ModelConstants; @@ -44,7 +45,7 @@ import static org.thingsboard.server.dao.service.Validator.*; @Service @Slf4j -public class UserServiceImpl implements UserService { +public class UserServiceImpl extends BaseEntityService implements UserService { private static final int DEFAULT_TOKEN_LENGTH = 30; @@ -159,6 +160,7 @@ public class UserServiceImpl implements UserService { validateId(userId, "Incorrect userId " + userId); UserCredentials userCredentials = userCredentialsDao.findByUserId(userId.getId()); userCredentialsDao.removeById(userCredentials.getUuidId()); + deleteEntityRelations(userId); userDao.removeById(userId.getId()); } diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql index febd1e94ef..b86031b76e 100644 --- a/dao/src/main/resources/schema.cql +++ b/dao/src/main/resources/schema.cql @@ -137,6 +137,13 @@ CREATE TABLE IF NOT EXISTS thingsboard.customer ( PRIMARY KEY (id, tenant_id) ); +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_title AS + SELECT * + from thingsboard.customer + WHERE tenant_id IS NOT NULL AND title IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, title, id ) + WITH CLUSTERING ORDER BY ( title ASC, id DESC ); + CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS SELECT * from thingsboard.customer @@ -195,6 +202,81 @@ 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) +) WITH CLUSTERING ORDER BY ( type ASC, id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.alarm_by_id AS + SELECT * + from thingsboard.alarm + WHERE tenant_id IS NOT NULL AND originator_id IS NOT NULL AND originator_type IS NOT NULL AND type IS NOT NULL + AND type IS NOT NULL AND id IS NOT NULL + PRIMARY KEY (id, tenant_id, originator_id, originator_type, type) + WITH CLUSTERING ORDER BY ( tenant_id ASC, originator_id ASC, originator_type ASC, type ASC); + +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/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql index cd00b1d97b..fc7c3155d9 100644 --- a/dao/src/main/resources/system-data.cql +++ b/dao/src/main/resources/system-data.cql @@ -80,7 +80,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio_panel', INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table', -'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Timestamp{{ h.dataKey.label }}
0 || ($index === 0 && showTimestamp)\" md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\" ng-bind-html=\"cellContent(source, $index, row, d)\">\n
\n
\n \n \n
\n
","templateCss":"table.md-table thead.md-head>tr.md-row {\n height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n height: 38px;\n}\n\n.md-table-pagination>* {\n height: 46px;\n}\n","controllerScript":"self.onInit = function() {\n \n var scope = self.ctx.$scope;\n \n self.ctx.filter = scope.$injector.get(\"$filter\");\n\n scope.sources = [];\n scope.sourceIndex = 0;\n scope.showTimestamp = self.ctx.settings.showTimestamp !== false;\n var origColor = self.ctx.widgetConfig.color || ''rgba(0, 0, 0, 0.87)'';\n var defaultColor = tinycolor(origColor);\n var mdDark = defaultColor.setAlpha(0.87).toRgbString();\n var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();\n var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();\n var mdDarkIcon = mdDarkSecondary;\n var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();\n \n var cssString = ''table.md-table th.md-column {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''table.md-table th.md-column md-icon.md-sort-icon {\\n''+\n ''color: '' + mdDarkDisabled + '';\\n''+\n ''}\\n''+\n ''table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\\n''+\n ''color: '' + mdDark + '';\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell {\\n''+\n ''color: '' + mdDark + '';\\n''+\n ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell.md-placeholder {\\n''+\n ''color: '' + mdDarkDisabled + '';\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''.md-table-pagination {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n ''}\\n''+\n ''.md-table-pagination .buttons md-icon {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''.md-table-pagination md-select:not([disabled]):focus .md-select-value {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}'';\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = ''ts-table-'' + hashCode(cssString);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, cssString);\n self.ctx.$container.addClass(namespace);\n \n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n var keyOffset = 0;\n for (var ds = 0; ds < self.ctx.datasources.length; ds++) {\n var source = {};\n var datasource = self.ctx.datasources[ds];\n source.keyStartIndex = keyOffset;\n keyOffset += datasource.dataKeys.length;\n source.keyEndIndex = keyOffset;\n source.datasource = datasource;\n source.data = [];\n source.rawData = [];\n source.query = {\n limit: 5,\n page: 1,\n order: ''-0''\n }\n source.ts = {\n header: [],\n count: 0,\n data: [],\n stylesInfo: [],\n contentsInfo: [],\n rowDataTemplate: {}\n }\n source.ts.rowDataTemplate[''Timestamp''] = null;\n for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n var dataKey = datasource.dataKeys[a];\n var keySettings = dataKey.settings;\n source.ts.header.push({\n index: a+1,\n dataKey: dataKey\n });\n source.ts.rowDataTemplate[dataKey.label] = null;\n\n var cellStyleFunction = null;\n var useCellStyleFunction = false;\n \n if (keySettings.useCellStyleFunction === true) {\n if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n try {\n cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n useCellStyleFunction = true;\n } catch (e) {\n cellStyleFunction = null;\n useCellStyleFunction = false;\n }\n }\n }\n\n source.ts.stylesInfo.push({\n useCellStyleFunction: useCellStyleFunction,\n cellStyleFunction: cellStyleFunction\n });\n \n var cellContentFunction = null;\n var useCellContentFunction = false;\n \n if (keySettings.useCellContentFunction === true) {\n if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {\n try {\n cellContentFunction = new Function(''value, rowData, filter'', keySettings.cellContentFunction);\n useCellContentFunction = true;\n } catch (e) {\n cellContentFunction = null;\n useCellContentFunction = false;\n }\n }\n }\n \n source.ts.contentsInfo.push({\n useCellContentFunction: useCellContentFunction,\n cellContentFunction: cellContentFunction\n });\n \n }\n scope.sources.push(source);\n }\n\n scope.onPaginate = function(source) {\n updatePage(source);\n }\n \n scope.onReorder = function(source) {\n reorder(source);\n updatePage(source);\n }\n \n scope.cellStyle = function(source, index, value) {\n var style = {};\n if (index > 0) {\n var styleInfo = source.ts.stylesInfo[index-1];\n if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n try {\n style = styleInfo.cellStyleFunction(value);\n } catch (e) {\n style = {};\n }\n }\n }\n return style;\n }\n\n scope.cellContent = function(source, index, row, value) {\n if (index === 0) {\n return self.ctx.filter(''date'')(value, ''yyyy-MM-dd HH:mm:ss'');\n } else {\n var strContent = '''';\n if (angular.isDefined(value)) {\n strContent = ''''+value;\n }\n var content = strContent;\n var contentInfo = source.ts.contentsInfo[index-1];\n if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {\n try {\n var rowData = source.ts.rowDataTemplate;\n rowData[''Timestamp''] = row[0];\n for (var h=0; h < source.ts.header.length; h++) {\n var headerInfo = source.ts.header[h];\n rowData[headerInfo.dataKey.name] = row[headerInfo.index];\n }\n content = contentInfo.cellContentFunction(value, rowData, self.ctx.filter);\n } catch (e) {\n content = strContent;\n }\n } \n return content;\n }\n }\n \n scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n if (newIndex != oldIndex) {\n updateSourceData(scope.sources[scope.sourceIndex]);\n } \n });\n}\n\nself.onDataUpdated = function() {\n var scope = self.ctx.$scope;\n for (var s=0; s < scope.sources.length; s++) {\n var source = scope.sources[s];\n source.rawData = self.ctx.data.slice(source.keyStartIndex, source.keyEndIndex);\n }\n updateSourceData(scope.sources[scope.sourceIndex]);\n scope.$digest();\n}\n\nself.onDestroy = function() {\n}\n\nfunction updatePage(source) {\n var startIndex = source.query.limit * (source.query.page - 1);\n source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n source.data = self.ctx.filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n var rowsMap = {};\n for (var d = 0; d < data.length; d++) {\n var columnData = data[d].data;\n for (var i = 0; i < columnData.length; i++) {\n var cellData = columnData[i];\n var timestamp = cellData[0];\n var row = rowsMap[timestamp];\n if (!row) {\n row = [];\n row[0] = timestamp;\n for (var c = 0; c < data.length; c++) {\n row[c+1] = undefined;\n }\n rowsMap[timestamp] = row;\n }\n row[d+1] = cellData[1];\n }\n }\n var rows = [];\n for (var t in rowsMap) {\n rows.push(rowsMap[t]);\n }\n return rows;\n}\n\nfunction updateSourceData(source) {\n source.data = convertData(source.rawData);\n source.ts.count = source.data.length;\n reorder(source);\n updatePage(source);\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: ''20px'',\\n color: ''#ffffff'',\\n background: color.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor(''blue'');\\n backgroundColor.setAlpha(value/100);\\n var color = ''blue'';\\n if (value > 50) {\\n color = ''white'';\\n }\\n \\n return {\\n paddingLeft: ''20px'',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}', +'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"\n","templateCss":"","controllerScript":"self.onInit = function() {\n \n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get(''utils'').guid();\n\n scope.config = {\n settings: self.ctx.settings,\n widgetConfig: self.ctx.widgetConfig\n }\n\n scope.datasources = self.ctx.datasources;\n scope.data = self.ctx.data;\n scope.tableId = \"table-\"+id;\n \n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.data = self.ctx.data;\n self.ctx.$scope.$broadcast(''timeseries-table-data-updated'', self.ctx.$scope.tableId);\n}\n\nself.onDestroy = function() {\n}","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: ''20px'',\\n color: ''#ffffff'',\\n background: color.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor(''blue'');\\n backgroundColor.setAlpha(value/100);\\n var color = ''blue'';\\n if (value > 50) {\\n color = ''white'';\\n }\\n \\n return {\\n paddingLeft: ''20px'',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}', 'Timeseries table' ); INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) 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..d724d7f0ad 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 @@ -32,6 +32,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; @@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.plugin.ComponentScope; 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.alarm.AlarmService; import org.thingsboard.server.dao.component.ComponentDescriptorService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -47,6 +49,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 +113,12 @@ public abstract class AbstractServiceTest { @Autowired protected EventService eventService; + @Autowired + protected RelationService relationService; + + @Autowired + protected AlarmService alarmService; + @Autowired private ComponentDescriptorService componentDescriptorService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java new file mode 100644 index 0000000000..8b905218a4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java @@ -0,0 +1,98 @@ +/** + * 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 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.Tenant; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +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 AlarmServiceTest extends AbstractServiceTest { + + public static final String TEST_ALARM = "TEST_ALARM"; + private TenantId tenantId; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + + @Test + public void testSaveAndFetchAlarm() 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(relationService.saveRelation(relation).get()); + + long ts = System.currentTimeMillis(); + Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) + .startTs(ts).build(); + + Alarm created = alarmService.createOrUpdateAlarm(alarm); + + Assert.assertNotNull(created); + Assert.assertNotNull(created.getId()); + Assert.assertNotNull(created.getOriginator()); + Assert.assertNotNull(created.getSeverity()); + Assert.assertNotNull(created.getStatus()); + + Assert.assertEquals(tenantId, created.getTenantId()); + Assert.assertEquals(childId, created.getOriginator()); + Assert.assertEquals(TEST_ALARM, created.getType()); + Assert.assertEquals(AlarmSeverity.CRITICAL, created.getSeverity()); + Assert.assertEquals(AlarmStatus.ACTIVE_UNACK, created.getStatus()); + Assert.assertEquals(ts, created.getStartTs()); + Assert.assertEquals(ts, created.getEndTs()); + Assert.assertEquals(0L, created.getAckTs()); + Assert.assertEquals(0L, created.getClearTs()); + + Alarm fetched = alarmService.findAlarmById(created.getId()).get(); + Assert.assertEquals(created, fetched); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java index 0271cad6df..96fdc477ad 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java @@ -33,11 +33,11 @@ import java.util.Collections; import java.util.List; public class CustomerServiceImplTest extends AbstractServiceTest { - + private IdComparator idComparator = new IdComparator<>(); - + private TenantId tenantId; - + @Before public void before() { Tenant tenant = new Tenant(); @@ -58,23 +58,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest { customer.setTenantId(tenantId); customer.setTitle("My customer"); Customer savedCustomer = customerService.saveCustomer(customer); - + Assert.assertNotNull(savedCustomer); Assert.assertNotNull(savedCustomer.getId()); Assert.assertTrue(savedCustomer.getCreatedTime() > 0); Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId()); Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle()); - - + + savedCustomer.setTitle("My new customer"); - + customerService.saveCustomer(savedCustomer); Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId()); Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle()); - + customerService.deleteCustomer(savedCustomer.getId()); } - + @Test public void testFindCustomerById() { Customer customer = new Customer(); @@ -86,21 +86,21 @@ public class CustomerServiceImplTest extends AbstractServiceTest { Assert.assertEquals(savedCustomer, foundCustomer); customerService.deleteCustomer(savedCustomer.getId()); } - + @Test(expected = DataValidationException.class) public void testSaveCustomerWithEmptyTitle() { Customer customer = new Customer(); customer.setTenantId(tenantId); customerService.saveCustomer(customer); } - + @Test(expected = DataValidationException.class) public void testSaveCustomerWithEmptyTenant() { Customer customer = new Customer(); customer.setTitle("My customer"); customerService.saveCustomer(customer); } - + @Test(expected = DataValidationException.class) public void testSaveCustomerWithInvalidTenant() { Customer customer = new Customer(); @@ -108,7 +108,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest { customer.setTenantId(new TenantId(UUIDs.timeBased())); customerService.saveCustomer(customer); } - + @Test(expected = DataValidationException.class) public void testSaveCustomerWithInvalidEmail() { Customer customer = new Customer(); @@ -117,7 +117,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest { customer.setEmail("invalid@mail"); customerService.saveCustomer(customer); } - + @Test public void testDeleteCustomer() { Customer customer = new Customer(); @@ -128,23 +128,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest { Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId()); Assert.assertNull(foundCustomer); } - + @Test public void testFindCustomersByTenantId() { Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); tenant = tenantService.saveTenant(tenant); - + TenantId tenantId = tenant.getId(); - + List customers = new ArrayList<>(); - for (int i=0;i<135;i++) { + for (int i = 0; i < 135; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - customer.setTitle("Customer"+i); + customer.setTitle("Customer" + i); customers.add(customerService.saveCustomer(customer)); } - + List loadedCustomers = new ArrayList<>(); TextPageLink pageLink = new TextPageLink(23); TextPageData pageData = null; @@ -155,47 +155,47 @@ public class CustomerServiceImplTest extends AbstractServiceTest { pageLink = pageData.getNextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(customers, idComparator); Collections.sort(loadedCustomers, idComparator); - + Assert.assertEquals(customers, loadedCustomers); - + customerService.deleteCustomersByTenantId(tenantId); pageLink = new TextPageLink(33); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - + tenantService.deleteTenant(tenantId); } - + @Test public void testFindCustomersByTenantIdAndTitle() { String title1 = "Customer title 1"; List customersTitle1 = new ArrayList<>(); - for (int i=0;i<143;i++) { + for (int i = 0; i < 143; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); - String title = title1+suffix; + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); + String title = title1 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); customersTitle1.add(customerService.saveCustomer(customer)); } String title2 = "Customer title 2"; List customersTitle2 = new ArrayList<>(); - for (int i=0;i<175;i++) { + for (int i = 0; i < 175; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); - String title = title2+suffix; + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); + String title = title2 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); customersTitle2.add(customerService.saveCustomer(customer)); } - + List loadedCustomersTitle1 = new ArrayList<>(); TextPageLink pageLink = new TextPageLink(15, title1); TextPageData pageData = null; @@ -206,12 +206,12 @@ public class CustomerServiceImplTest extends AbstractServiceTest { pageLink = pageData.getNextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(customersTitle1, idComparator); Collections.sort(loadedCustomersTitle1, idComparator); - + Assert.assertEquals(customersTitle1, loadedCustomersTitle1); - + List loadedCustomersTitle2 = new ArrayList<>(); pageLink = new TextPageLink(4, title2); do { @@ -224,22 +224,22 @@ public class CustomerServiceImplTest extends AbstractServiceTest { Collections.sort(customersTitle2, idComparator); Collections.sort(loadedCustomersTitle2, idComparator); - + Assert.assertEquals(customersTitle2, loadedCustomersTitle2); for (Customer customer : loadedCustomersTitle1) { customerService.deleteCustomer(customer.getId()); } - + pageLink = new TextPageLink(4, title1); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); - + for (Customer customer : loadedCustomersTitle2) { customerService.deleteCustomer(customer.getId()); } - + pageLink = new TextPageLink(4, title2); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java new file mode 100644 index 0000000000..376a88dc49 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.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 RelationServiceTest 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/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java index 79deaeead1..af03ac57da 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java @@ -71,6 +71,7 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { String eventType = STATS; String eventUid = "be41c7a3-31f5-11e7-9cfd-2786e6aa2046"; Event event = eventDao.findEvent(tenantId, new DeviceId(entityId), eventType, eventUid); + eventDao.find().stream().forEach(System.out::println); assertNotNull("Event expected to be not null", event); assertEquals("be41c7a2-31f5-11e7-9cfd-2786e6aa2046", event.getId().getId().toString()); } 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 ae49cb1c23..779aa55bd5 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 @@ -62,7 +62,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); @@ -91,7 +91,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())); } @@ -110,7 +110,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()); @@ -122,7 +122,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()); @@ -134,7 +134,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()); @@ -147,7 +147,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()); @@ -160,7 +160,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()); @@ -173,7 +173,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()); @@ -189,15 +189,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/dao/src/test/resources/dbunit/events.xml b/dao/src/test/resources/dbunit/events.xml index 2c02c19564..943b45af2e 100644 --- a/dao/src/test/resources/dbunit/events.xml +++ b/dao/src/test/resources/dbunit/events.xml @@ -3,7 +3,7 @@ id="be41c7a2-31f5-11e7-9cfd-2786e6aa2046" tenant_id="be41c7a0-31f5-11e7-9cfd-2786e6aa2046" entity_id="be41c7a1-31f5-11e7-9cfd-2786e6aa2046" - entity_type="1" + entity_type="7" event_type="STATS" event_uid="be41c7a3-31f5-11e7-9cfd-2786e6aa2046" /> @@ -11,7 +11,7 @@ id="be41c7a4-31f5-11e7-9cfd-2786e6aa2046" tenant_id="be41c7a0-31f5-11e7-9cfd-2786e6aa2046" entity_id="be41c7a1-31f5-11e7-9cfd-2786e6aa2046" - entity_type="1" + entity_type="7" event_type="STATS" event_uid="be41c7a5-31f5-11e7-9cfd-2786e6aa2046" /> 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..8864ae1f7d 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,111 +56,94 @@ 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(deviceId, queries, getTsKvListCallback(msg)); + 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", ""); + + 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 @@ -162,9 +151,26 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { 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 +191,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 +208,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 +242,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 +315,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/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java index 4e7793e0d3..ac168d085b 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java @@ -30,7 +30,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg; import org.thingsboard.server.extensions.api.rules.RuleException; import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; /** @@ -51,7 +51,7 @@ public class TimePlugin extends AbstractPlugin implemen String reply; if (!StringUtils.isEmpty(format)) { - reply = "\"" + formatter.format(LocalDateTime.now()) + "\""; + reply = "\"" + formatter.format(ZonedDateTime.now()) + "\""; } else { reply = Long.toString(System.currentTimeMillis()); } 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/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java index 797ebf5cb7..a07c80d74e 100644 --- a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java +++ b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java @@ -55,6 +55,13 @@ public class RestApiCallPlugin extends AbstractPlugin { + log.debug("Adding header to request object. Key = {}, Value = {}", h.getKey(), h.getValue()); + this.headers.add(h.getKey(), h.getValue()); + }); + } + init(); } diff --git a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java index 2b20e9bec1..cfd23b8c20 100644 --- a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java +++ b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java @@ -16,6 +16,9 @@ package org.thingsboard.server.extensions.rest.plugin; import lombok.Data; +import org.thingsboard.server.extensions.core.plugin.KeyValuePluginProperties; + +import java.util.List; @Data public class RestApiCallPluginConfiguration { @@ -27,4 +30,6 @@ public class RestApiCallPluginConfiguration { private String userName; private String password; + + private List headers; } diff --git a/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json b/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json index e0e4d1814d..06f8559cf0 100644 --- a/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json +++ b/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json @@ -30,6 +30,24 @@ "password": { "title": "Password", "type": "string" + }, + "headers": { + "title": "Request Headers", + "type": "array", + "items": { + "title": "Request Header", + "type": "object", + "properties": { + "key": { + "title": "Key", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + } } }, "required": [ @@ -62,6 +80,7 @@ { "key": "password", "type": "password" - } + }, + "headers" ] } \ No newline at end of file diff --git a/tools/pom.xml b/tools/pom.xml index 3e76f4675f..96235e2cbe 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -54,40 +54,4 @@ - - - - org.apache.maven.plugins - maven-shade-plugin - - - package - - shade - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - org.thingsboard.client.tools.MqttStressTestTool - - - - - - - - - diff --git a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java index f742e6131e..d635dbcb3e 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java +++ b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java @@ -27,6 +27,7 @@ import org.springframework.http.client.support.HttpRequestWrapper; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -76,6 +77,12 @@ public class RestClient implements ClientHttpRequestInterceptor { return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); } + + public Device assignDevice(CustomerId customerId, DeviceId deviceId) { + return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class, + customerId.toString(), deviceId.toString()).getBody(); + } + public DeviceCredentials getCredentials(DeviceId id) { return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody(); } diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 76cdf1ac5d..36868c5906 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -247,7 +247,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private MqttMessage createUnSubAckMessage(int msgId) { MqttFixedHeader mqttFixedHeader = - new MqttFixedHeader(SUBACK, false, AT_LEAST_ONCE, false, 0); + new MqttFixedHeader(UNSUBACK, false, AT_LEAST_ONCE, false, 0); MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); } diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java index 9444cdd1c0..af109f652b 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java @@ -40,6 +40,8 @@ import java.security.cert.CertificateException; */ public class MqttTransportServerInitializer extends ChannelInitializer { + private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024; + private final SessionMsgProcessor processor; private final DeviceService deviceService; private final DeviceAuthService authService; @@ -63,7 +65,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer0) { + 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/customer.service.js b/ui/src/app/api/customer.service.js index 133021bc40..8a3fe66a16 100644 --- a/ui/src/app/api/customer.service.js +++ b/ui/src/app/api/customer.service.js @@ -18,12 +18,14 @@ export default angular.module('thingsboard.api.customer', []) .name; /*@ngInject*/ -function CustomerService($http, $q) { +function CustomerService($http, $q, types) { var service = { getCustomers: getCustomers, getCustomer: getCustomer, - getCustomerTitle: getCustomerTitle, + getShortCustomerInfo: getShortCustomerInfo, + applyAssignedCustomersInfo: applyAssignedCustomersInfo, + applyAssignedCustomerInfo: applyAssignedCustomerInfo, deleteCustomer: deleteCustomer, saveCustomer: saveCustomer } @@ -72,6 +74,88 @@ function CustomerService($http, $q) { return deferred.promise; } + function getShortCustomerInfo(customerId) { + var deferred = $q.defer(); + var url = '/api/customer/' + customerId + '/shortInfo'; + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail(response) { + deferred.reject(response.data); + }); + return deferred.promise; + } + + function applyAssignedCustomersInfo(items) { + var deferred = $q.defer(); + var assignedCustomersMap = {}; + function loadNextCustomerInfoOrComplete(i) { + i++; + if (i < items.length) { + loadNextCustomerInfo(i); + } else { + deferred.resolve(items); + } + } + + function loadNextCustomerInfo(i) { + var item = items[i]; + item.assignedCustomer = {}; + if (item.customerId && item.customerId.id != types.id.nullUid) { + item.assignedCustomer.id = item.customerId.id; + var assignedCustomer = assignedCustomersMap[item.customerId.id]; + if (assignedCustomer){ + item.assignedCustomer = assignedCustomer; + loadNextCustomerInfoOrComplete(i); + } else { + getShortCustomerInfo(item.customerId.id).then( + function success(info) { + assignedCustomer = { + id: item.customerId.id, + title: info.title, + isPublic: info.isPublic + }; + assignedCustomersMap[assignedCustomer.id] = assignedCustomer; + item.assignedCustomer = assignedCustomer; + loadNextCustomerInfoOrComplete(i); + }, + function fail() { + loadNextCustomerInfoOrComplete(i); + } + ); + } + } else { + loadNextCustomerInfoOrComplete(i); + } + } + if (items.length > 0) { + loadNextCustomerInfo(0); + } else { + deferred.resolve(items); + } + return deferred.promise; + } + + function applyAssignedCustomerInfo(items, customerId) { + var deferred = $q.defer(); + getShortCustomerInfo(customerId).then( + function success(info) { + var assignedCustomer = { + id: customerId, + title: info.title, + isPublic: info.isPublic + } + items.forEach(function(item) { + item.assignedCustomer = assignedCustomer; + }); + deferred.resolve(items); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + function saveCustomer(customer) { var deferred = $q.defer(); var url = '/api/customer'; diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js index be450ed477..b2a7897916 100644 --- a/ui/src/app/api/dashboard.service.js +++ b/ui/src/app/api/dashboard.service.js @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.dashboard', []) .factory('dashboardService', DashboardService).name; /*@ngInject*/ -function DashboardService($http, $q) { +function DashboardService($http, $q, $location, customerService) { var service = { assignDashboardToCustomer: assignDashboardToCustomer, @@ -27,7 +27,9 @@ function DashboardService($http, $q) { getTenantDashboards: getTenantDashboards, deleteDashboard: deleteDashboard, saveDashboard: saveDashboard, - unassignDashboardFromCustomer: unassignDashboardFromCustomer + unassignDashboardFromCustomer: unassignDashboardFromCustomer, + makeDashboardPublic: makeDashboardPublic, + getPublicDashboardLink: getPublicDashboardLink } return service; @@ -45,7 +47,15 @@ function DashboardService($http, $q) { url += '&textOffset=' + pageLink.textOffset; } $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); + customerService.applyAssignedCustomersInfo(response.data.data).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); }, function fail() { deferred.reject(); }); @@ -65,7 +75,15 @@ function DashboardService($http, $q) { url += '&textOffset=' + pageLink.textOffset; } $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); + customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); }, function fail() { deferred.reject(); }); @@ -92,8 +110,8 @@ function DashboardService($http, $q) { var url = '/api/dashboard/' + dashboardId; $http.get(url, null).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -103,8 +121,8 @@ function DashboardService($http, $q) { var url = '/api/dashboard'; $http.post(url, dashboard).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -114,8 +132,8 @@ function DashboardService($http, $q) { var url = '/api/dashboard/' + dashboardId; $http.delete(url).then(function success() { deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -123,10 +141,10 @@ function DashboardService($http, $q) { function assignDashboardToCustomer(customerId, dashboardId) { var deferred = $q.defer(); var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId; - $http.post(url, null).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); + $http.post(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -134,12 +152,33 @@ function DashboardService($http, $q) { function unassignDashboardFromCustomer(dashboardId) { var deferred = $q.defer(); var url = '/api/customer/dashboard/' + dashboardId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); + $http.delete(url).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function makeDashboardPublic(dashboardId) { + var deferred = $q.defer(); + var url = '/api/customer/public/dashboard/' + dashboardId; + $http.post(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } + function getPublicDashboardLink(dashboard) { + var url = $location.protocol() + '://' + $location.host(); + var port = $location.port(); + if (port != 80 && port != 443) { + url += ":" + port; + } + url += "/dashboards/" + dashboard.id.id + "?publicId=" + dashboard.customerId.id; + return url; + } + } diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js index 6041cbd946..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; } @@ -58,14 +57,15 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t var datasourceSubscription = { datasourceType: datasource.type, dataKeys: subscriptionDataKeys, - type: listener.widget.type + type: listener.subscriptionType }; - if (listener.widget.type === types.widgetType.timeseries.value) { + 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, @@ -266,6 +267,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic type: types.dataKeyType.timeseries, onData: function (data) { if (data.data) { + for (var key in data.data) { + var keyData = data.data[key]; + data.data[key] = $filter('orderBy')(keyData, '+this[0]'); + } onData(data.data, types.dataKeyType.timeseries, true); } }, @@ -278,7 +283,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic } else { subscriptionCommand = { - deviceId: datasourceSubscription.deviceId, + entityType: datasourceSubscription.entityType, + entityId: datasourceSubscription.entityId, keys: tsKeys }; @@ -324,7 +330,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic if (attrKeys.length > 0) { subscriptionCommand = { - deviceId: datasourceSubscription.deviceId, + entityType: datasourceSubscription.entityType, + entityId: datasourceSubscription.entityId, keys: attrKeys }; @@ -400,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 b369e90b57..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, telemetryWebsocketService, types) { - - - var deviceAttributesSubscriptionMap = {}; +function DeviceService($http, $q, attributeService, customerService, types) { var service = { assignDeviceToCustomer: assignDeviceToCustomer, @@ -31,15 +28,12 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic getCustomerDevices: getCustomerDevices, getDevice: getDevice, getDevices: getDevices, - processDeviceAliases: processDeviceAliases, - checkDeviceAlias: checkDeviceAlias, getDeviceCredentials: getDeviceCredentials, - getDeviceKeys: getDeviceKeys, - getDeviceTimeseriesValues: getDeviceTimeseriesValues, getTenantDevices: getTenantDevices, saveDevice: saveDevice, saveDeviceCredentials: saveDeviceCredentials, unassignDeviceFromCustomer: unassignDeviceFromCustomer, + makeDevicePublic: makeDevicePublic, getDeviceAttributes: getDeviceAttributes, subscribeForDeviceAttributes: subscribeForDeviceAttributes, unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes, @@ -51,7 +45,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic return service; - function getTenantDevices(pageLink, config) { + function getTenantDevices(pageLink, applyCustomersInfo, config) { var deferred = $q.defer(); var url = '/api/tenant/devices?limit=' + pageLink.limit; if (angular.isDefined(pageLink.textSearch)) { @@ -64,14 +58,26 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic url += '&textOffset=' + pageLink.textOffset; } $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); + 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 getCustomerDevices(customerId, pageLink) { + function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) { var deferred = $q.defer(); var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit; if (angular.isDefined(pageLink.textSearch)) { @@ -83,18 +89,35 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic if (angular.isDefined(pageLink.textOffset)) { url += '&textOffset=' + pageLink.textOffset; } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); + $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 getDevice(deviceId, ignoreErrors) { + function getDevice(deviceId, ignoreErrors, config) { var deferred = $q.defer(); var url = '/api/device/' + deviceId; - $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) { + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.get(url, config).then(function success(response) { deferred.resolve(response.data); }, function fail(response) { deferred.reject(response.data); @@ -102,7 +125,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic return deferred.promise; } - function getDevices(deviceIds) { + function getDevices(deviceIds, config) { var deferred = $q.defer(); var ids = ''; for (var i=0;i 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).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); - } 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'; $http.post(url, device).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -312,8 +167,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic var url = '/api/device/' + deviceId; $http.delete(url).then(function success() { deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -323,8 +178,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic var url = '/api/device/' + deviceId + '/credentials'; $http.get(url, null).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -334,8 +189,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic var url = '/api/device/credentials'; $http.post(url, deviceCredentials).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -343,10 +198,10 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic function assignDeviceToCustomer(customerId, deviceId) { var deferred = $q.defer(); var url = '/api/customer/' + customerId + '/device/' + deviceId; - $http.post(url, null).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); + $http.post(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -354,206 +209,43 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic function unassignDeviceFromCustomer(deviceId) { var deferred = $q.defer(); var url = '/api/customer/device/' + deviceId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); - }); - 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 -1) { + this.registrations.splice(index, 1); + } + } + } + + initRpc() { + if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { + this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; + if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) { + this.targetDeviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId; + } + } else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) { + this.targetDeviceId = this.targetDeviceIds[0]; + } + + if (this.targetDeviceId) { + this.rpcEnabled = true; + } else { + this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false; + } + this.callbacks.rpcStateChanged(this); + } + + clearRpcError() { + this.rpcRejection = null; + this.rpcErrorText = null; + this.callbacks.onRpcErrorCleared(this); + } + + sendOneWayCommand(method, params, timeout) { + return this.sendCommand(true, method, params, timeout); + } + + sendTwoWayCommand(method, params, timeout) { + return this.sendCommand(false, method, params, timeout); + } + + sendCommand(oneWayElseTwoWay, method, params, timeout) { + if (!this.rpcEnabled) { + return this.ctx.$q.reject(); + } + + if (this.rpcRejection && this.rpcRejection.status !== 408) { + this.rpcRejection = null; + this.rpcErrorText = null; + this.callbacks.onRpcErrorCleared(this); + } + + var subscription = this; + + var requestBody = { + method: method, + params: params + }; + + if (timeout && timeout > 0) { + requestBody.timeout = timeout; + } + + var deferred = this.ctx.$q.defer(); + this.executingRpcRequest = true; + this.callbacks.rpcStateChanged(this); + if (this.ctx.$scope.widgetEditMode) { + this.ctx.$timeout(function() { + subscription.executingRpcRequest = false; + subscription.callbacks.rpcStateChanged(subscription); + if (oneWayElseTwoWay) { + deferred.resolve(); + } else { + deferred.resolve(requestBody); + } + }, 500); + } else { + this.executingPromises.push(deferred.promise); + var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand; + targetSendFunction(this.targetDeviceId, requestBody).then( + function success(responseBody) { + subscription.rpcRejection = null; + subscription.rpcErrorText = null; + var index = subscription.executingPromises.indexOf(deferred.promise); + if (index >= 0) { + subscription.executingPromises.splice( index, 1 ); + } + subscription.executingRpcRequest = subscription.executingPromises.length > 0; + subscription.callbacks.onRpcSuccess(subscription); + deferred.resolve(responseBody); + }, + function fail(rejection) { + var index = subscription.executingPromises.indexOf(deferred.promise); + if (index >= 0) { + subscription.executingPromises.splice( index, 1 ); + } + subscription.executingRpcRequest = subscription.executingPromises.length > 0; + subscription.callbacks.rpcStateChanged(subscription); + if (!subscription.executingRpcRequest || rejection.status === 408) { + subscription.rpcRejection = rejection; + if (rejection.status === 408) { + subscription.rpcErrorText = 'Device is offline.'; + } else { + subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; + if (rejection.data && rejection.data.length > 0) { + subscription.rpcErrorText += '
'; + subscription.rpcErrorText += rejection.data; + } + } + subscription.callbacks.onRpcFailed(subscription); + } + deferred.reject(rejection); + } + ); + } + return deferred.promise; + } + + updateDataVisibility(index) { + var hidden = this.legendData.data[index].hidden; + if (hidden) { + this.hiddenData[index].data = this.data[index].data; + this.data[index].data = []; + } else { + this.data[index].data = this.hiddenData[index].data; + this.hiddenData[index].data = []; + } + 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'](); + this.cafs['dataUpdated'] = null; + } + var subscription = this; + this.cafs['dataUpdated'] = this.ctx.tbRaf(function() { + try { + subscription.callbacks.onDataUpdated(subscription, apply); + } catch (e) { + subscription.callbacks.onDataUpdateError(subscription, e); + } + }); + if (apply) { + this.ctx.$scope.$digest(); + } + } + + updateTimewindowConfig(newTimewindow) { + this.timeWindowConfig = newTimewindow; + } + + onResetTimewindow() { + if (this.useDashboardTimewindow) { + this.ctx.dashboardTimewindowApi.onResetTimewindow(); + } else { + if (this.originalTimewindow) { + this.stopWatchingTimewindow(); + this.timeWindowConfig = angular.copy(this.originalTimewindow); + this.originalTimewindow = null; + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); + this.unsubscribe(); + this.subscribe(); + this.startWatchingTimewindow(); + } + } + } + + onUpdateTimewindow(startTimeMs, endTimeMs) { + if (this.useDashboardTimewindow) { + this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); + } else { + this.stopWatchingTimewindow(); + if (!this.originalTimewindow) { + this.originalTimewindow = angular.copy(this.timeWindowConfig); + } + this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs); + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); + this.unsubscribe(); + this.subscribe(); + this.startWatchingTimewindow(); + } + } + + notifyDataLoading() { + this.loadingData = true; + this.callbacks.dataLoading(this); + } + + notifyDataLoaded() { + this.loadingData = false; + this.callbacks.dataLoading(this); + } + + updateTimewindow() { + this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; + if (this.subscriptionTimewindow.realtimeWindowMs) { + this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff; + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; + } else if (this.subscriptionTimewindow.fixedWindow) { + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; + } + } + + updateRealtimeSubscription(subscriptionTimewindow) { + if (subscriptionTimewindow) { + this.subscriptionTimewindow = subscriptionTimewindow; + } else { + this.subscriptionTimewindow = + this.ctx.timeService.createSubscriptionTimewindow( + this.timeWindowConfig, + this.timeWindow.stDiff); + } + this.updateTimewindow(); + return this.subscriptionTimewindow; + } + + dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { + this.notifyDataLoaded(); + var update = true; + var currentData; + if (this.displayLegend && this.legendData.data[datasourceIndex + dataKeyIndex].hidden) { + currentData = this.hiddenData[datasourceIndex + dataKeyIndex]; + } else { + currentData = this.data[datasourceIndex + dataKeyIndex]; + } + if (this.type === this.ctx.types.widgetType.latest.value) { + var prevData = currentData.data; + if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { + var prevValue = prevData[0][1]; + if (prevValue === sourceData.data[0][1]) { + update = false; + } + } + } + if (update) { + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { + this.updateTimewindow(); + } + currentData.data = sourceData.data; + if (this.caulculateLegendData) { + this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply); + } + this.onDataUpdated(apply); + } + } + + updateLegend(dataIndex, data, apply) { + var legendKeyData = this.legendData.data[dataIndex]; + if (this.legendConfig.showMin) { + legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), this.decimals, this.units); + } + if (this.legendConfig.showMax) { + legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), this.decimals, this.units); + } + if (this.legendConfig.showAvg) { + legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), this.decimals, this.units); + } + if (this.legendConfig.showTotal) { + legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), this.decimals, this.units); + } + this.callbacks.legendDataUpdated(this, apply !== false); + } + + subscribe() { + if (this.type === this.ctx.types.widgetType.rpc.value) { + return; + } + this.notifyDataLoading(); + if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { + this.updateRealtimeSubscription(); + if (this.subscriptionTimewindow.fixedWindow) { + this.onDataUpdated(); + } + } + var index = 0; + for (var i = 0; i < this.datasources.length; i++) { + var datasource = this.datasources[i]; + if (angular.isFunction(datasource)) + continue; + var entityId = null; + var entityType = null; + if (datasource.type === this.ctx.types.datasourceType.entity) { + var aliasName = null; + 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; + } + } + } + } + } else { + datasource.name = datasource.name || this.ctx.types.datasourceType.function; + } + for (var dk = 0; dk < datasource.dataKeys.length; dk++) { + updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, entityName, aliasName); + } + + var subscription = this; + + var listener = { + subscriptionType: this.type, + subscriptionTimewindow: this.subscriptionTimewindow, + datasource: datasource, + entityType: entityType, + entityId: entityId, + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { + subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); + }, + updateRealtimeSubscription: function () { + this.subscriptionTimewindow = subscription.updateRealtimeSubscription(); + return this.subscriptionTimewindow; + }, + setRealtimeSubscription: function (subscriptionTimewindow) { + subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); + }, + datasourceIndex: index + }; + + for (var a = 0; a < datasource.dataKeys.length; a++) { + this.data[index + a].data = []; + } + + index += datasource.dataKeys.length; + + this.datasourceListeners.push(listener); + this.ctx.datasourceService.subscribeToDatasource(listener); + } + } + + unsubscribe() { + if (this.type !== this.ctx.types.widgetType.rpc.value) { + for (var i = 0; i < this.datasourceListeners.length; i++) { + var listener = this.datasourceListeners[i]; + this.ctx.datasourceService.unsubscribeFromDatasource(listener); + } + this.datasourceListeners = []; + } + } + + 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 entityId = null; + var entityType = null; + var aliasName = null; + 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(entityId, listener.entityId) || + !angular.equals(entityType, listener.entityType) || + !angular.equals(aliasName, listener.datasource.name)) { + subscriptionsChanged = true; + break; + } + } + } + if (subscriptionsChanged) { + this.unsubscribe(); + this.subscribe(); + } + } + + destroy() { + this.unsubscribe(); + for (var cafId in this.cafs) { + if (this.cafs[cafId]) { + this.cafs[cafId](); + this.cafs[cafId] = null; + } + } + this.registrations.forEach(function (registration) { + registration(); + }); + this.registrations = []; + } + +} + +const varsRegex = /\$\{([^\}]*)\}/g; + +function updateDataKeyLabel(dataKey, dsName, entityName, aliasName) { + var pattern = dataKey.pattern; + var label = dataKey.pattern; + var match = varsRegex.exec(pattern); + while (match !== null) { + var variable = match[0]; + 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(entityName); + } else if (variableName === 'aliasName') { + label = label.split(variable).join(aliasName); + } + match = varsRegex.exec(pattern); + } + dataKey.label = label; +} + +function calculateMin(data) { + if (data.length > 0) { + var result = Number(data[0][1]); + for (var i=1;i 0) { + var result = Number(data[0][1]); + for (var i=1;i 0) { + return calculateTotal(data)/data.length; + } else { + return null; + } +} + +function calculateTotal(data) { + if (data.length > 0) { + var result = 0; + for (var i = 0; i < data.length; i++) { + result += Number(data[i][1]); + } + return result; + } else { + return null; + } +} diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index ad401df47f..4dc41f7d99 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -22,9 +22,10 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin, .name; /*@ngInject*/ -function UserService($http, $q, $rootScope, adminService, dashboardService, toast, store, jwtHelper, $translate, $state) { +function UserService($http, $q, $rootScope, adminService, dashboardService, loginService, toast, store, jwtHelper, $translate, $state, $location) { var currentUser = null, currentUserDetails = null, + lastPublicDashboardId = null, allowedDashboardIds = [], userLoaded = false; @@ -33,6 +34,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas var service = { deleteUser: deleteUser, getAuthority: getAuthority, + isPublic: isPublic, + getPublicId: getPublicId, + parsePublicId: parsePublicId, isAuthenticated: isAuthenticated, getCurrentUser: getCurrentUser, getCustomerUsers: getCustomerUsers, @@ -51,18 +55,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas updateAuthorizationHeader: updateAuthorizationHeader, gotoDefaultPlace: gotoDefaultPlace, forceDefaultPlace: forceDefaultPlace, - logout: logout + updateLastPublicDashboardId: updateLastPublicDashboardId, + logout: logout, + reloadUser: reloadUser } - loadUser(true).then(function success() { - notifyUserLoaded(); - }, function fail() { - notifyUserLoaded(); - }); + reloadUser(); return service; - function updateAndValidateToken(token, prefix) { + function reloadUser() { + userLoaded = false; + loadUser(true).then(function success() { + notifyUserLoaded(); + }, function fail() { + notifyUserLoaded(); + }); + } + + function updateAndValidateToken(token, prefix, notify) { var valid = false; var tokenData = jwtHelper.decodeToken(token); var issuedAt = tokenData.iat; @@ -76,7 +87,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas valid = true; } } - if (!valid) { + if (!valid && notify) { $rootScope.$broadcast('unauthenticated'); } } @@ -91,6 +102,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) { currentUser = null; currentUserDetails = null; + lastPublicDashboardId = null; allowedDashboardIds = []; if (!jwtToken) { clearTokenData(); @@ -98,8 +110,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas $rootScope.$broadcast('unauthenticated', doLogout); } } else { - updateAndValidateToken(jwtToken, 'jwt_token'); - updateAndValidateToken(refreshToken, 'refresh_token'); + updateAndValidateToken(jwtToken, 'jwt_token', true); + updateAndValidateToken(refreshToken, 'refresh_token', true); if (notify) { loadUser(false).then(function success() { $rootScope.$broadcast('authenticated'); @@ -213,13 +225,58 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas } } + function isPublic() { + if (currentUser) { + return currentUser.isPublic; + } else { + return false; + } + } + + function getPublicId() { + if (isPublic()) { + return currentUser.sub; + } else { + return null; + } + } + + function parsePublicId() { + var token = getJwtToken(); + if (token) { + var tokenData = jwtHelper.decodeToken(token); + if (tokenData && tokenData.isPublic) { + return tokenData.sub; + } + } + return null; + } + function isUserLoaded() { return userLoaded; } function loadUser(doTokenRefresh) { + var deferred = $q.defer(); - if (!currentUser) { + + function fetchAllowedDashboardIds() { + var pageLink = {limit: 100}; + dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then( + function success(result) { + var dashboards = result.data; + for (var d=0;d -1) { + if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) { + if (to.name === 'home.profile') { + if (userHasProfile()) { return false; } else { return true; } + } else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) { + return false; + } else { + return true; } } } @@ -395,11 +459,12 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas if (currentUser && isAuthenticated()) { var place = 'home.links'; if (currentUser.authority === 'CUSTOMER_USER') { - if (currentUserDetails && - currentUserDetails.additionalInfo && - currentUserDetails.additionalInfo.defaultDashboardId) { + if (userHasDefaultDashboard()) { place = 'home.dashboards.dashboard'; params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId}; + } else if (isPublic()) { + place = 'home.dashboards.dashboard'; + params = {dashboardId: lastPublicDashboardId}; } } else if (currentUser.authority === 'SYS_ADMIN') { adminService.checkUpdates().then( @@ -416,4 +481,27 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas } } + function userHasDefaultDashboard() { + return currentUserDetails && + currentUserDetails.additionalInfo && + currentUserDetails.additionalInfo.defaultDashboardId; + } + + function userForceFullscreen() { + return (currentUser && currentUser.isPublic) || + (currentUserDetails.additionalInfo && + currentUserDetails.additionalInfo.defaultDashboardFullscreen && + currentUserDetails.additionalInfo.defaultDashboardFullscreen === true); + } + + function userHasProfile() { + return currentUser && !currentUser.isPublic; + } + + function updateLastPublicDashboardId(dashboardId) { + if (isPublic()) { + lastPublicDashboardId = dashboardId; + } + } + } diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index fb8565c0fa..21c38e254c 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -17,7 +17,8 @@ import $ from 'jquery'; import moment from 'moment'; import tinycolor from 'tinycolor2'; -import thinsboardLedLight from '../components/led-light.directive'; +import thingsboardLedLight from '../components/led-light.directive'; +import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; import TbFlot from '../widget/lib/flot-widget'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; @@ -31,7 +32,8 @@ 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', thinsboardLedLight, thingsboardTypes, thingsboardUtils]) +export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, + thingsboardTypes, thingsboardUtils]) .factory('widgetService', WidgetService) .name; @@ -539,6 +541,10 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ ' }\n\n' + + ' self.useCustomDatasources = function() {\n\n' + + + ' }\n\n' + + ' self.onResize = function() {\n\n' + ' }\n\n' + @@ -579,6 +585,11 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); } + if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) { + result.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); + } else { + result.useCustomDatasources = false; + } return result; } catch (e) { utils.processWidgetException(e); @@ -617,6 +628,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ if (widgetType.dataKeySettingsSchema) { widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema; } + widgetInfo.useCustomDatasources = widgetType.useCustomDatasources; putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem); deferred.resolve(widgetInfo); diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js index 9b2ad251a7..7f7e7a5b30 100644 --- a/ui/src/app/app.config.js +++ b/ui/src/app/app.config.js @@ -16,6 +16,9 @@ import injectTapEventPlugin from 'react-tap-event-plugin'; import UrlHandler from './url.handler'; import addLocaleKorean from './locale/locale.constant-ko'; +import addLocaleChinese from './locale/locale.constant-zh'; +import addLocaleRussian from './locale/locale.constant-ru'; +import addLocaleSpanish from './locale/locale.constant-es'; /* eslint-disable import/no-unresolved, import/default */ @@ -50,11 +53,21 @@ export default function AppConfig($provide, $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); addLocaleKorean(locales); + addLocaleChinese(locales); + addLocaleRussian(locales); + addLocaleSpanish(locales); + var $window = angular.injector(['ng']).get('$window'); var lang = $window.navigator.language || $window.navigator.userLanguage; if (lang === 'ko') { $translateProvider.useSanitizeValueStrategy(null); $translateProvider.preferredLanguage('ko_KR'); + } else if (lang === 'zh') { + $translateProvider.useSanitizeValueStrategy(null); + $translateProvider.preferredLanguage('zh_CN'); + } else if (lang === 'es') { + $translateProvider.useSanitizeValueStrategy(null); + $translateProvider.preferredLanguage('es_ES'); } for (var langKey in locales) { diff --git a/ui/src/app/app.js b/ui/src/app/app.js index e3b36e3c6b..f67be331a9 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -19,6 +19,7 @@ import angular from 'angular'; import ngMaterial from 'angular-material'; import ngMdIcons from 'angular-material-icons'; import ngCookies from 'angular-cookies'; +import angularSocialshare from 'angular-socialshare'; import 'angular-translate'; import 'angular-translate-loader-static-files'; import 'angular-translate-storage-local'; @@ -52,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'; @@ -61,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'; @@ -82,6 +88,7 @@ angular.module('thingsboard', [ ngMaterial, ngMdIcons, ngCookies, + angularSocialshare, 'pascalprecht.translate', 'mdColorPicker', mdPickers, @@ -103,6 +110,7 @@ angular.module('thingsboard', [ thingsboardMenu, thingsboardRaf, thingsboardUtils, + thingsboardDashboardUtils, thingsboardTypes, thingsboardApiTime, thingsboardKeyboardShortcut, @@ -112,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/app.run.js b/ui/src/app/app.run.js index 02fe8fd8fb..1e36600427 100644 --- a/ui/src/app/app.run.js +++ b/ui/src/app/app.run.js @@ -36,7 +36,7 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, } initWatchers(); - + function initWatchers() { $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) { if (doLogout) { @@ -55,8 +55,39 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, }); $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) { + + function waitForUserLoaded() { + if ($rootScope.userLoadedHandle) { + $rootScope.userLoadedHandle(); + } + $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () { + $rootScope.userLoadedHandle(); + $state.go(to.name, params); + }); + } + + function reloadUserFromPublicId() { + userService.setUserFromJwtToken(null, null, false); + waitForUserLoaded(); + userService.reloadUser(); + } + + var locationSearch = $location.search(); + var publicId = locationSearch.publicId; + if (userService.isUserLoaded() === true) { if (userService.isAuthenticated()) { + if (userService.isPublic()) { + if (userService.parsePublicId() !== publicId) { + evt.preventDefault(); + if (publicId && publicId.length > 0) { + reloadUserFromPublicId(); + } else { + userService.logout(); + } + return; + } + } if (userService.forceDefaultPlace(to, params)) { evt.preventDefault(); gotoDefaultPlace(params); @@ -75,7 +106,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, } } } else { - if (to.module === 'private') { + if (publicId && publicId.length > 0) { + evt.preventDefault(); + reloadUserFromPublicId(); + } else if (to.module === 'private') { evt.preventDefault(); if (to.url === '/home' || to.url === '/') { $state.go('login', params); @@ -86,19 +120,17 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, } } else { evt.preventDefault(); - if ($rootScope.userLoadedHandle) { - $rootScope.userLoadedHandle(); - } - $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () { - $rootScope.userLoadedHandle(); - $state.go(to.name, params); - }); + waitForUserLoaded(); } }) $rootScope.pageTitle = 'Thingsboard'; - $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to) { + $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) { + if (userService.isPublic() && to.name === 'home.dashboards.dashboard') { + $location.search('publicId', userService.getPublicId()); + userService.updateLastPublicDashboardId(params.dashboardId); + } if (angular.isDefined(to.data.pageTitle)) { $translate(to.data.pageTitle).then(function (translation) { $rootScope.pageTitle = 'Thingsboard | ' + translation; 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 3fc5202b51..b324cbfa45 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -104,7 +104,11 @@ function Utils($mdColorPalette, $rootScope, $window, types) { parseException: parseException, processWidgetException: processWidgetException, isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, - filterSearchTextEntities: filterSearchTextEntities + filterSearchTextEntities: filterSearchTextEntities, + guid: guid, + isLocalUrl: isLocalUrl, + validateDatasources: validateDatasources, + createKey: createKey } return service; @@ -276,4 +280,70 @@ function Utils($mdColorPalette, $rootScope, $window, types) { deferred.resolve(response); } + function guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + } + + function genNextColor(datasources) { + var index = 0; + if (datasources) { + for (var i = 0; i < datasources.length; i++) { + var datasource = datasources[i]; + index += datasource.dataKeys.length; + } + } + return getMaterialColor(index); + } + + 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; + } + } + + 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 = { + name: keyInfo.name, + type: type, + label: keyInfo.label || keyInfo.name, + color: genNextColor(datasources), + funcBody: keyInfo.funcBody, + settings: {}, + _hash: Math.random() + } + return dataKey; + } + } diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index 7c7a5526dc..93b889a89c 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -182,12 +182,14 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t vm.dashboardTimewindowApi = { onResetTimewindow: function() { - if (vm.originalDashboardTimewindow) { + $timeout(function() { + if (vm.originalDashboardTimewindow) { $timeout(function() { vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); vm.originalDashboardTimewindow = null; }, 0); - } + } + }, 0); }, onUpdateTimewindow: function(startTimeMs, endTimeMs) { if (!vm.originalDashboardTimewindow) { @@ -272,8 +274,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/device-filter.directive.js b/ui/src/app/components/device-filter.directive.js index ecf12cd542..0233c95b02 100644 --- a/ui/src/app/components/device-filter.directive.js +++ b/ui/src/app/components/device-filter.directive.js @@ -41,7 +41,7 @@ function DeviceFilter($compile, $templateCache, $q, deviceService) { var deferred = $q.defer(); - deviceService.getTenantDevices(pageLink).then(function success(result) { + deviceService.getTenantDevices(pageLink, false).then(function success(result) { deferred.resolve(result.data); }, function fail() { deferred.reject(); 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/grid.tpl.html b/ui/src/app/components/grid.tpl.html index 1cf0085576..c29c2e5b1a 100644 --- a/ui/src/app/components/grid.tpl.html +++ b/ui/src/app/components/grid.tpl.html @@ -49,7 +49,7 @@ - {{ action.details() }} + {{ action.details( rowItem[n] ) }} @@ -62,7 +62,7 @@ 0) { + return utils.isLocalUrl(vm.shareLink); + } else { + return true; + } + } + +} \ No newline at end of file diff --git a/ui/src/app/components/socialshare-panel.tpl.html b/ui/src/app/components/socialshare-panel.tpl.html new file mode 100644 index 0000000000..7b9560de36 --- /dev/null +++ b/ui/src/app/components/socialshare-panel.tpl.html @@ -0,0 +1,62 @@ + + +
+ + + + {{ 'action.share-via' | translate:{provider:'Facebook'} }} + + + + + + {{ 'action.share-via' | translate:{provider:'Twitter'} }} + + + + + + {{ 'action.share-via' | translate:{provider:'Linkedin'} }} + + + + + + {{ 'action.share-via' | translate:{provider:'Reddit'} }} + + +
\ No newline at end of file diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js index 61a5d14558..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, @@ -76,6 +76,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.forceExpandDatasources = false; } + if (angular.isUndefined(scope.isDataEnabled)) { + scope.isDataEnabled = true; + } + scope.currentSettingsSchema = {}; scope.currentSettings = angular.copy(scope.emptySettingsSchema); @@ -108,7 +112,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ? ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value; scope.legendConfig = ngModelCtrl.$viewValue.legendConfig; - if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) { + if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value + && scope.isDataEnabled) { if (scope.datasources) { scope.datasources.splice(0, scope.datasources.length); } else { @@ -119,12 +124,12 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]}); } } - } else if (scope.widgetType === types.widgetType.rpc.value) { + } 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; } @@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; var valid; - if (scope.widgetType === types.widgetType.rpc.value) { + if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0; ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); - } else if (scope.widgetType !== types.widgetType.static.value) { + } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { valid = value && value.datasources && value.datasources.length > 0; ngModelCtrl.$setValidity('datasources', valid); } @@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.$watch('datasources', function () { if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value - && scope.widgetType !== types.widgetType.static.value) { + && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; if (value.datasources) { value.datasources.splice(0, value.datasources.length); @@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti }, true); scope.$watch('targetDeviceAlias.value', function () { - if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value) { + if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; if (scope.targetDeviceAlias.value) { value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; @@ -264,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: [] }; } @@ -359,13 +364,14 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti require: "^ngModel", scope: { forceExpandDatasources: '=?', + isDataEnabled: '=?', 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 66b440fe09..808e07b487 100644 --- a/ui/src/app/components/widget-config.tpl.html +++ b/ui/src/app/components/widget-config.tpl.html @@ -31,7 +31,7 @@ + ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled"> {{ 'widget-config.datasources' | translate }} @@ -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})"> + ng-show="widgetType === types.widgetType.rpc.value && isDataEnabled"> {{ 'widget-config.target-device' | translate }} - - + 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 bf500b1746..42299c6ca8 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget.controller.js @@ -15,12 +15,13 @@ */ import $ from 'jquery'; import 'javascript-detect-element-resize/detect-element-resize'; +import Subscription from '../api/subscription'; /* eslint-disable angular/angularelement */ /*@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; @@ -34,19 +35,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q $scope.rpcErrorText = null; $scope.rpcEnabled = false; $scope.executingRpcRequest = false; - $scope.executingPromises = []; var gridsterItemInited = false; - var datasourceListeners = []; - var targetDeviceAliasId = null; - var targetDeviceId = null; - var originalTimewindow = null; - var subscriptionTimewindow = null; var cafs = {}; - var varsRegex = /\$\{([^\}]*)\}/g; - /* * data = array of datasourceData * datasourceData = { @@ -54,8 +47,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q * dataKey, { name, config } * data = array of [time, value] * } - * - * */ var widgetContext = { @@ -71,22 +62,71 @@ export default function WidgetController($scope, $timeout, $window, $element, $q settings: widget.config.settings, units: widget.config.units || '', decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2, - datasources: angular.copy(widget.config.datasources), - data: [], - hiddenData: [], - timeWindow: { - stDiff: stDiff - }, + subscriptions: {}, + defaultSubscription: null, timewindowFunctions: { - onUpdateTimewindow: onUpdateTimewindow, - onResetTimewindow: onResetTimewindow + onUpdateTimewindow: function(startTimeMs, endTimeMs) { + if (widgetContext.defaultSubscription) { + widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs); + } + }, + onResetTimewindow: function() { + if (widgetContext.defaultSubscription) { + widgetContext.defaultSubscription.onResetTimewindow(); + } + } + }, + subscriptionApi: { + createSubscription: function(options, subscribe) { + return createSubscription(options, subscribe); + }, + + + // type: "timeseries" or "latest" or "rpc" + /* subscriptionInfo = [ + { + entityType: "" + entityId: "" + entityName: "" + timeseries: [{ name: "", label: "" }, ..] + attributes: [{ name: "", label: "" }, ..] + } + .. + ]*/ + + // options = { + // timeWindowConfig, + // useDashboardTimewindow, + // legendConfig, + // decimals, + // units, + // callbacks [ onDataUpdated(subscription, apply) ] + // } + // + + createSubscriptionFromInfo: function (type, subscriptionsInfo, options, useDefaultComponents, subscribe) { + return createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe); + }, + removeSubscription: function(id) { + var subscription = widgetContext.subscriptions[id]; + if (subscription) { + subscription.destroy(); + delete widgetContext.subscriptions[id]; + } + } }, controlApi: { sendOneWayCommand: function(method, params, timeout) { - return sendCommand(true, method, params, timeout); + if (widgetContext.defaultSubscription) { + return widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout); + } + return null; }, sendTwoWayCommand: function(method, params, timeout) { - return sendCommand(false, method, params, timeout); + if (widgetContext.defaultSubscription) { + return widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout); + } + return null; } }, utils: { @@ -94,7 +134,27 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } }; + var subscriptionContext = { + $scope: $scope, + $q: $q, + $filter: $filter, + $timeout: $timeout, + tbRaf: tbRaf, + timeService: timeService, + deviceService: deviceService, + datasourceService: datasourceService, + utils: utils, + widgetUtils: widgetContext.utils, + dashboardTimewindowApi: dashboardTimewindowApi, + types: types, + stDiff: stDiff, + aliasesInfo: aliasesInfo + }; + var widgetTypeInstance; + + vm.useCustomDatasources = false; + try { widgetTypeInstance = new widgetType(widgetContext); } catch (e) { @@ -119,19 +179,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q if (!widgetTypeInstance.onDestroy) { widgetTypeInstance.onDestroy = function() {}; } - - //var bounds = {top: 0, left: 0, bottom: 0, right: 0}; - //TODO: widgets visibility - /*var visible = false;*/ - - $scope.clearRpcError = function() { - $scope.rpcRejection = null; - $scope.rpcErrorText = null; + if (widgetTypeInstance.useCustomDatasources) { + vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); } - vm.gridsterItemInitialized = gridsterItemInitialized; - //TODO: widgets visibility + + //var bounds = {top: 0, left: 0, bottom: 0, right: 0}; + /*var visible = false;*/ /*vm.visibleRectChanged = visibleRectChanged; function visibleRectChanged(newVisibleRect) { @@ -139,23 +194,226 @@ export default function WidgetController($scope, $timeout, $window, $element, $q updateVisibility(); }*/ + $scope.clearRpcError = function() { + if (widgetContext.defaultSubscription) { + widgetContext.defaultSubscription.clearRpcError(); + } + } + + vm.gridsterItemInitialized = gridsterItemInitialized; + initialize(); - function handleWidgetException(e) { - $log.error(e); - $scope.widgetErrorData = utils.processWidgetException(e); + + /* + options = { + type, + targetDeviceAliasIds, // RPC + targetDeviceIds, // RPC + datasources, + timeWindowConfig, + useDashboardTimewindow, + legendConfig, + decimals, + units, + callbacks + } + */ + + function createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe) { + var deferred = $q.defer(); + options.type = type; + + if (useDefaultComponents) { + defaultComponentsOptions(options); + } else { + if (!options.timeWindowConfig) { + options.useDashboardTimewindow = true; + } + } + + entityService.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then( + function (datasources) { + options.datasources = datasources; + var subscription = createSubscription(options, subscribe); + if (useDefaultComponents) { + defaultSubscriptionOptions(subscription, options); + } + deferred.resolve(subscription); + } + ); + return deferred.promise; + } + + function createSubscription(options, subscribe) { + options.dashboardTimewindow = dashboardTimewindow; + var subscription = + new Subscription(subscriptionContext, options); + widgetContext.subscriptions[subscription.id] = subscription; + if (subscribe) { + subscription.subscribe(); + } + return subscription; + } + + function defaultComponentsOptions(options) { + options.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow) + ? widget.config.useDashboardTimewindow : true; + + options.timeWindowConfig = options.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow; + options.legendConfig = null; + + if ($scope.displayLegend) { + options.legendConfig = $scope.legendConfig; + } + options.decimals = widgetContext.decimals; + options.units = widgetContext.units; + + options.callbacks = { + onDataUpdated: function() { + widgetTypeInstance.onDataUpdated(); + }, + onDataUpdateError: function(subscription, e) { + handleWidgetException(e); + }, + dataLoading: function(subscription) { + if ($scope.loadingData !== subscription.loadingData) { + $scope.loadingData = subscription.loadingData; + } + }, + legendDataUpdated: function(subscription, apply) { + if (apply) { + $scope.$digest(); + } + }, + timeWindowUpdated: function(subscription, timeWindowConfig) { + widget.config.timewindow = timeWindowConfig; + $scope.$apply(); + } + } + } + + function defaultSubscriptionOptions(subscription, options) { + if (!options.useDashboardTimewindow) { + $scope.$watch(function () { + return widget.config.timewindow; + }, function (newTimewindow, prevTimewindow) { + if (!angular.equals(newTimewindow, prevTimewindow)) { + subscription.updateTimewindowConfig(widget.config.timewindow); + } + }); + } + if ($scope.displayLegend) { + $scope.legendData = subscription.legendData; + } } - function notifyDataLoaded() { - if ($scope.loadingData === true) { + function createDefaultSubscription() { + var subscription; + var options; + if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { + options = { + type: widget.type, + datasources: angular.copy(widget.config.datasources) + }; + defaultComponentsOptions(options); + + subscription = createSubscription(options); + + defaultSubscriptionOptions(subscription, options); + + // backward compatibility + + widgetContext.datasources = subscription.datasources; + widgetContext.data = subscription.data; + widgetContext.hiddenData = subscription.hiddenData; + widgetContext.timeWindow = subscription.timeWindow; + + } else if (widget.type === types.widgetType.rpc.value) { + $scope.loadingData = false; + options = { + type: widget.type, + targetDeviceAliasIds: widget.config.targetDeviceAliasIds + } + options.callbacks = { + rpcStateChanged: function(subscription) { + $scope.rpcEnabled = subscription.rpcEnabled; + $scope.executingRpcRequest = subscription.executingRpcRequest; + }, + onRpcSuccess: function(subscription) { + $scope.executingRpcRequest = subscription.executingRpcRequest; + $scope.rpcErrorText = subscription.rpcErrorText; + $scope.rpcRejection = subscription.rpcRejection; + }, + onRpcFailed: function(subscription) { + $scope.executingRpcRequest = subscription.executingRpcRequest; + $scope.rpcErrorText = subscription.rpcErrorText; + $scope.rpcRejection = subscription.rpcRejection; + }, + onRpcErrorCleared: function() { + $scope.rpcErrorText = null; + $scope.rpcRejection = null; + } + } + subscription = createSubscription(options); + } else if (widget.type === types.widgetType.static.value) { $scope.loadingData = false; } + if (subscription) { + widgetContext.defaultSubscription = subscription; + } } - function notifyDataLoading() { - if ($scope.loadingData === false) { - $scope.loadingData = true; + + function initialize() { + + if (!vm.useCustomDatasources) { + createDefaultSubscription(); + } else { + $scope.loadingData = false; } + + $scope.$on('toggleDashboardEditMode', function (event, isEdit) { + onEditModeChanged(isEdit); + }); + + addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef + + $scope.$watch(function () { + return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder; + }, function () { + //updateBounds(); + $scope.$emit("widgetPositionChanged", widget); + }); + + $scope.$on('gridster-item-resized', function (event, item) { + if (!widgetContext.isMobile) { + widget.sizeX = item.sizeX; + widget.sizeY = item.sizeY; + } + }); + + $scope.$on('mobileModeChanged', function (event, newIsMobile) { + 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(); + }); + } + + function handleWidgetException(e) { + $log.error(e); + $scope.widgetErrorData = utils.processWidgetException(e); } function onInit() { @@ -166,42 +424,12 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } catch (e) { handleWidgetException(e); } - if (widgetContext.dataUpdatePending) { - widgetContext.dataUpdatePending = false; - onDataUpdated(); + if (!vm.useCustomDatasources && widgetContext.defaultSubscription) { + widgetContext.defaultSubscription.subscribe(); } } } - function updateTimewindow() { - widgetContext.timeWindow.interval = subscriptionTimewindow.aggregation.interval || 1000; - if (subscriptionTimewindow.realtimeWindowMs) { - widgetContext.timeWindow.maxTime = (new Date).getTime() + widgetContext.timeWindow.stDiff; - widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs; - } else if (subscriptionTimewindow.fixedWindow) { - widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs; - widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs; - } - } - - function onDataUpdated() { - if (widgetContext.inited) { - if (cafs['dataUpdate']) { - cafs['dataUpdate'](); - cafs['dataUpdate'] = null; - } - cafs['dataUpdate'] = tbRaf(function() { - try { - widgetTypeInstance.onDataUpdated(); - } catch (e) { - handleWidgetException(e); - } - }); - } else { - widgetContext.dataUpdatePending = true; - } - } - function checkSize() { var width = widgetContext.$containerParent.width(); var height = widgetContext.$containerParent.height(); @@ -289,11 +517,35 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } } + function isNumeric(val) { + return (val - parseFloat( val ) + 1) >= 0; + } + + function formatValue(value, dec, units) { + if (angular.isDefined(value) && + value !== null && isNumeric(value)) { + var formatted = value; + if (angular.isDefined(dec)) { + formatted = formatted.toFixed(dec); + } + formatted = (formatted * 1).toString(); + if (angular.isDefined(units) && units.length > 0) { + formatted += ' ' + units; + } + return formatted; + } else { + return ''; + } + } + function onDestroy() { - unsubscribe(); + for (var id in widgetContext.subscriptions) { + var subscription = widgetContext.subscriptions[id]; + subscription.destroy(); + } + widgetContext.subscriptions = []; if (widgetContext.inited) { widgetContext.inited = false; - widgetContext.dataUpdatePending = false; for (var cafId in cafs) { if (cafs[cafId]) { cafs[cafId](); @@ -308,244 +560,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } } - function onRestart() { - onDestroy(); - onInit(); - } - -/* scope.legendData = { - keys: [], - data: [] - - key: { - label: '', - color: '' - dataIndex: 0 - } - data: { - min: null, - max: null, - avg: null, - total: null - } - };*/ - - - function initialize() { - - $scope.caulculateLegendData = $scope.displayLegend && - widget.type === types.widgetType.timeseries.value && - ($scope.legendConfig.showMin === true || - $scope.legendConfig.showMax === true || - $scope.legendConfig.showAvg === true || - $scope.legendConfig.showTotal === true); - - if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { var dataIndex = 0; - for (var i = 0; i < widgetContext.datasources.length; i++) { - var datasource = widgetContext.datasources[i]; - for (var a = 0; a < datasource.dataKeys.length; a++) { - var dataKey = datasource.dataKeys[a]; - dataKey.pattern = angular.copy(dataKey.label); - var datasourceData = { - datasource: datasource, - dataKey: dataKey, - data: [] - }; - widgetContext.data.push(datasourceData); - widgetContext.hiddenData.push({data: []}); - if ($scope.displayLegend) { - var legendKey = { - dataKey: dataKey, - dataIndex: dataIndex++ - }; - $scope.legendData.keys.push(legendKey); - var legendKeyData = { - min: null, - max: null, - avg: null, - total: null, - hidden: false - }; - $scope.legendData.data.push(legendKeyData); - } - } - } - if ($scope.displayLegend) { - $scope.legendData.keys = $filter('orderBy')($scope.legendData.keys, '+label'); - $scope.$on('legendDataHiddenChanged', function (event, index) { - event.stopPropagation(); - var hidden = $scope.legendData.data[index].hidden; - if (hidden) { - widgetContext.hiddenData[index].data = widgetContext.data[index].data; - widgetContext.data[index].data = []; - } else { - widgetContext.data[index].data = widgetContext.hiddenData[index].data; - widgetContext.hiddenData[index].data = []; - } - onDataUpdated(); - }); - } - } else if (widget.type === types.widgetType.rpc.value) { - if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { - targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; - if (aliasesInfo.deviceAliases[targetDeviceAliasId]) { - targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId; - } - } - if (targetDeviceId) { - $scope.rpcEnabled = true; - } else { - $scope.rpcEnabled = $scope.widgetEditMode ? true : false; - } - } - - $scope.$on('toggleDashboardEditMode', function (event, isEdit) { - onEditModeChanged(isEdit); - }); - - addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef - - $scope.$watch(function () { - return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder; - }, function () { - //updateBounds(); - $scope.$emit("widgetPositionChanged", widget); - }); - - $scope.$on('gridster-item-resized', function (event, item) { - if (!widgetContext.isMobile) { - widget.sizeX = item.sizeX; - widget.sizeY = item.sizeY; - } - }); - - $scope.$on('mobileModeChanged', function (event, newIsMobile) { - onMobileModeChanged(newIsMobile); - }); - - $scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) { - aliasesInfo = newAliasesInfo; - if (widget.type === types.widgetType.rpc.value) { - if (targetDeviceAliasId) { - var deviceId = null; - if (aliasesInfo.deviceAliases[targetDeviceAliasId]) { - deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId; - } - if (!angular.equals(deviceId, targetDeviceId)) { - targetDeviceId = deviceId; - if (targetDeviceId) { - $scope.rpcEnabled = true; - } else { - $scope.rpcEnabled = $scope.widgetEditMode ? true : false; - } - onRestart(); - } - } - } else { - checkSubscriptions(); - } - }); - - $scope.$on("$destroy", function () { - removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef - onDestroy(); - }); - - if (widget.type === types.widgetType.timeseries.value) { - widgetContext.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow) - ? widget.config.useDashboardTimewindow : true; - if (widgetContext.useDashboardTimewindow) { - $scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { - if (!angular.equals(dashboardTimewindow, newDashboardTimewindow)) { - dashboardTimewindow = newDashboardTimewindow; - unsubscribe(); - subscribe(); - } - }); - } else { - $scope.$watch(function () { - return widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow; - }, function (newTimewindow, prevTimewindow) { - if (!angular.equals(newTimewindow, prevTimewindow)) { - unsubscribe(); - subscribe(); - } - }); - } - } - subscribe(); - } - - function sendCommand(oneWayElseTwoWay, method, params, timeout) { - if (!$scope.rpcEnabled) { - return $q.reject(); - } - - if ($scope.rpcRejection && $scope.rpcRejection.status !== 408) { - $scope.rpcRejection = null; - $scope.rpcErrorText = null; - } - - var requestBody = { - method: method, - params: params - }; - - if (timeout && timeout > 0) { - requestBody.timeout = timeout; - } - - var deferred = $q.defer(); - $scope.executingRpcRequest = true; - if ($scope.widgetEditMode) { - $timeout(function() { - $scope.executingRpcRequest = false; - if (oneWayElseTwoWay) { - deferred.resolve(); - } else { - deferred.resolve(requestBody); - } - }, 500); - } else { - $scope.executingPromises.push(deferred.promise); - var targetSendFunction = oneWayElseTwoWay ? deviceService.sendOneWayRpcCommand : deviceService.sendTwoWayRpcCommand; - targetSendFunction(targetDeviceId, requestBody).then( - function success(responseBody) { - $scope.rpcRejection = null; - $scope.rpcErrorText = null; - var index = $scope.executingPromises.indexOf(deferred.promise); - if (index >= 0) { - $scope.executingPromises.splice( index, 1 ); - } - $scope.executingRpcRequest = $scope.executingPromises.length > 0; - deferred.resolve(responseBody); - }, - function fail(rejection) { - var index = $scope.executingPromises.indexOf(deferred.promise); - if (index >= 0) { - $scope.executingPromises.splice( index, 1 ); - } - $scope.executingRpcRequest = $scope.executingPromises.length > 0; - if (!$scope.executingRpcRequest || rejection.status === 408) { - $scope.rpcRejection = rejection; - if (rejection.status === 408) { - $scope.rpcErrorText = 'Device is offline.'; - } else { - $scope.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; - if (rejection.data && rejection.data.length > 0) { - $scope.rpcErrorText += '
'; - $scope.rpcErrorText += rejection.data; - } - } - } - deferred.reject(rejection); - } - ); - } - return deferred.promise; - } - //TODO: widgets visibility /*function updateVisibility(forceRedraw) { if (visibleRect) { @@ -584,285 +599,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q onRedraw(); }*/ - function onResetTimewindow() { - if (widgetContext.useDashboardTimewindow) { - dashboardTimewindowApi.onResetTimewindow(); - } else { - if (originalTimewindow) { - widget.config.timewindow = angular.copy(originalTimewindow); - originalTimewindow = null; - } - } - } - - function onUpdateTimewindow(startTimeMs, endTimeMs) { - if (widgetContext.useDashboardTimewindow) { - dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); - } else { - if (!originalTimewindow) { - originalTimewindow = angular.copy(widget.config.timewindow); - } - widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs); - } - } - - function dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { - notifyDataLoaded(); - var update = true; - var currentData; - if ($scope.displayLegend && $scope.legendData.data[datasourceIndex + dataKeyIndex].hidden) { - currentData = widgetContext.hiddenData[datasourceIndex + dataKeyIndex]; - } else { - currentData = widgetContext.data[datasourceIndex + dataKeyIndex]; - } - if (widget.type === types.widgetType.latest.value) { - var prevData = currentData.data; - if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { - var prevValue = prevData[0][1]; - if (prevValue === sourceData.data[0][1]) { - update = false; - } - } - } - if (update) { - if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) { - updateTimewindow(); - } - currentData.data = sourceData.data; - onDataUpdated(); - if ($scope.caulculateLegendData) { - updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply); - } - } - if (apply) { - $scope.$digest(); - } - } - - function updateLegend(dataIndex, data, apply) { - var legendKeyData = $scope.legendData.data[dataIndex]; - if ($scope.legendConfig.showMin) { - legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showMax) { - legendKeyData.max = formatValue(calculateMax(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showAvg) { - legendKeyData.avg = formatValue(calculateAvg(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showTotal) { - legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units); - } - $scope.$broadcast('legendDataUpdated', apply !== false); - } - - function isNumeric(val) { - return (val - parseFloat( val ) + 1) >= 0; - } - - function formatValue(value, dec, units) { - if (angular.isDefined(value) && - value !== null && isNumeric(value)) { - var formatted = value; - if (angular.isDefined(dec)) { - formatted = formatted.toFixed(dec); - } - formatted = (formatted * 1).toString(); - if (angular.isDefined(units) && units.length > 0) { - formatted += ' ' + units; - } - return formatted; - } else { - return ''; - } - } - - function calculateMin(data) { - if (data.length > 0) { - var result = Number(data[0][1]); - for (var i=1;i 0) { - var result = Number(data[0][1]); - for (var i=1;i 0) { - return calculateTotal(data)/data.length; - } else { - return null; - } - } - - function calculateTotal(data) { - if (data.length > 0) { - var result = 0; - for (var i = 0; i < data.length; i++) { - result += Number(data[i][1]); - } - return result; - } else { - return null; - } - } - - function checkSubscriptions() { - if (widget.type !== types.widgetType.rpc.value) { - var subscriptionsChanged = false; - for (var i = 0; i < datasourceListeners.length; i++) { - var listener = datasourceListeners[i]; - var deviceId = null; - var aliasName = null; - if (listener.datasource.type === types.datasourceType.device) { - if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) { - deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId; - aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias; - } - if (!angular.equals(deviceId, listener.deviceId) || - !angular.equals(aliasName, listener.datasource.name)) { - subscriptionsChanged = true; - break; - } - } - } - if (subscriptionsChanged) { - unsubscribe(); - subscribe(); - } - } - } - - function unsubscribe() { - if (widget.type !== types.widgetType.rpc.value) { - for (var i = 0; i < datasourceListeners.length; i++) { - var listener = datasourceListeners[i]; - datasourceService.unsubscribeFromDatasource(listener); - } - datasourceListeners = []; - } - } - - function updateRealtimeSubscription(_subscriptionTimewindow) { - if (_subscriptionTimewindow) { - subscriptionTimewindow = _subscriptionTimewindow; - } else { - subscriptionTimewindow = - timeService.createSubscriptionTimewindow( - widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow, - widgetContext.timeWindow.stDiff); - } - updateTimewindow(); - return subscriptionTimewindow; - } - - function hasTimewindow() { - if (widgetContext.useDashboardTimewindow) { - return angular.isDefined(dashboardTimewindow); - } else { - return angular.isDefined(widget.config.timewindow); - } - } - - function updateDataKeyLabel(dataKey, deviceName, aliasName) { - var pattern = dataKey.pattern; - var label = dataKey.pattern; - var match = varsRegex.exec(pattern); - while (match !== null) { - var variable = match[0]; - var variableName = match[1]; - if (variableName === 'deviceName') { - label = label.split(variable).join(deviceName); - } else if (variableName === 'aliasName') { - label = label.split(variable).join(aliasName); - } - match = varsRegex.exec(pattern); - } - dataKey.label = label; - } - - function subscribe() { - if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { - notifyDataLoading(); - if (widget.type === types.widgetType.timeseries.value && - hasTimewindow()) { - updateRealtimeSubscription(); - if (subscriptionTimewindow.fixedWindow) { - onDataUpdated(); - } - } - var index = 0; - for (var i = 0; i < widgetContext.datasources.length; i++) { - var datasource = widgetContext.datasources[i]; - if (angular.isFunction(datasource)) - continue; - var deviceId = null; - if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) { - if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) { - deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId; - datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; - var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; - var deviceName = ''; - var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId]; - for (var d=0;d -
{{ item | contactShort }}
\ No newline at end of file +
{{ item | contactShort }}
\ No newline at end of file diff --git a/ui/src/app/customer/customer-fieldset.tpl.html b/ui/src/app/customer/customer-fieldset.tpl.html index 62ef6b3f35..3facd0f7a8 100644 --- a/ui/src/app/customer/customer-fieldset.tpl.html +++ b/ui/src/app/customer/customer-fieldset.tpl.html @@ -15,13 +15,24 @@ limitations under the License. --> -{{ 'customer.manage-users' | translate }} +{{ 'customer.manage-users' | translate }} +{{ 'customer.manage-assets' | translate }} {{ 'customer.manage-devices' | translate }} {{ 'customer.manage-dashboards' | translate }} -{{ 'customer.delete' | 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 57386604f3..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 = [ { @@ -30,14 +30,37 @@ export default function CustomerController(customerService, $state, $stateParams }, name: function() { return $translate.instant('user.users') }, details: function() { return $translate.instant('customer.manage-customer-users') }, - icon: "account_circle" + icon: "account_circle", + isEnabled: function(customer) { + 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); }, name: function() { return $translate.instant('device.devices') }, - details: function() { return $translate.instant('customer.manage-customer-devices') }, + details: function(customer) { + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { + return $translate.instant('customer.manage-public-devices') + } else { + return $translate.instant('customer.manage-customer-devices') + } + }, icon: "devices_other" }, { @@ -45,7 +68,13 @@ export default function CustomerController(customerService, $state, $stateParams openCustomerDashboards($event, item); }, name: function() { return $translate.instant('dashboard.dashboards') }, - details: function() { return $translate.instant('customer.manage-customer-dashboards') }, + details: function(customer) { + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { + return $translate.instant('customer.manage-public-dashboards') + } else { + return $translate.instant('customer.manage-customer-dashboards') + } + }, icon: "dashboard" }, { @@ -54,12 +83,17 @@ export default function CustomerController(customerService, $state, $stateParams }, name: function() { return $translate.instant('action.delete') }, details: function() { return $translate.instant('customer.delete') }, - icon: "delete" + icon: "delete", + isEnabled: function(customer) { + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + } } ]; var vm = this; + vm.types = types; + vm.customerGridConfig = { refreshParamsFunc: null, @@ -86,7 +120,19 @@ export default function CustomerController(customerService, $state, $stateParams addItemText: function() { return $translate.instant('customer.add-customer-text') }, noItemsText: function() { return $translate.instant('customer.no-customers-text') }, - itemDetailsText: function() { return $translate.instant('customer.customer-details') } + itemDetailsText: function(customer) { + if (customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic)) { + return $translate.instant('customer.customer-details') + } else { + return ''; + } + }, + isSelectionEnabled: function (customer) { + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + }, + isDetailsReadOnly: function (customer) { + return customer && customer.additionalInfo && customer.additionalInfo.isPublic; + } }; if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { @@ -98,6 +144,7 @@ export default function CustomerController(customerService, $state, $stateParams } vm.openCustomerUsers = openCustomerUsers; + vm.openCustomerAssets = openCustomerAssets; vm.openCustomerDevices = openCustomerDevices; vm.openCustomerDashboards = openCustomerDashboards; @@ -148,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 f23711ae5e..e48934b1e7 100644 --- a/ui/src/app/customer/customer.directive.js +++ b/ui/src/app/customer/customer.directive.js @@ -20,11 +20,29 @@ 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) { + scope.isPublic = scope.customer.additionalInfo.isPublic; + } else { + scope.isPublic = false; + } + } + }); + $compile(element.contents())(scope); + } return { restrict: "E", @@ -34,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-card.tpl.html b/ui/src/app/dashboard/dashboard-card.tpl.html index 9f6cd00b7a..636786758e 100644 --- a/ui/src/app/dashboard/dashboard-card.tpl.html +++ b/ui/src/app/dashboard/dashboard-card.tpl.html @@ -15,5 +15,6 @@ limitations under the License. --> -
{{'dashboard.assignedToCustomer' | translate}} '{{vm.customerTitle}}'
+
{{'dashboard.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'
+
{{'dashboard.public' | translate}}
diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html index 6325e17229..954d881588 100644 --- a/ui/src/app/dashboard/dashboard-fieldset.tpl.html +++ b/ui/src/app/dashboard/dashboard-fieldset.tpl.html @@ -15,25 +15,50 @@ limitations under the License. --> +{{ 'dashboard.export' | translate }} +{{ 'dashboard.make-public' | translate }} {{ 'dashboard.assign-to-customer' | translate }} -{{ 'dashboard.unassign-from-customer' | translate }} -{{ 'dashboard.export' | translate }} + class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }} {{ 'dashboard.delete' | translate }} - + ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'"> +
+ + +
+ + + + + + + + {{ 'dashboard.copy-public-link' | translate }} + + +
+
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js index aac6da3dd0..ee107cc4be 100644 --- a/ui/src/app/dashboard/dashboard-settings.controller.js +++ b/ui/src/app/dashboard/dashboard-settings.controller.js @@ -31,6 +31,18 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti vm.gridSettings.showTitle = true; } + if (angular.isUndefined(vm.gridSettings.showEntitiesSelect)) { + vm.gridSettings.showEntitiesSelect = true; + } + + if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) { + vm.gridSettings.showDashboardTimewindow = true; + } + + if (angular.isUndefined(vm.gridSettings.showDashboardExport)) { + vm.gridSettings.showDashboardExport = true; + } + vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)'; vm.gridSettings.columns = vm.gridSettings.columns || 24; diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html index 2ed25d3edf..6ae47465d9 100644 --- a/ui/src/app/dashboard/dashboard-settings.tpl.html +++ b/ui/src/app/dashboard/dashboard-settings.tpl.html @@ -48,6 +48,17 @@ md-color-history="false" > +
+ {{ 'dashboard.display-entities-selection' | translate }} + + {{ 'dashboard.display-dashboard-timewindow' | translate }} + + {{ 'dashboard.display-dashboard-export' | translate }} + +
- + - {{ 'dashboard.export' | translate }} file_download - + - - - + ng-model="vm.aliasesInfo.entityAliases" + entity-aliases-info="vm.aliasesInfo.entityAliasesInfo"> + + - {{ 'device.aliases' | translate }} + {{ 'entity.aliases' | translate }} - devices_other + devices_other @@ -304,6 +309,6 @@ diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js index 3ef96a7198..134baffc59 100644 --- a/ui/src/app/dashboard/dashboards.controller.js +++ b/ui/src/app/dashboard/dashboards.controller.js @@ -19,11 +19,33 @@ import addDashboardTemplate from './add-dashboard.tpl.html'; import dashboardCard from './dashboard-card.tpl.html'; import assignToCustomerTemplate from './assign-to-customer.tpl.html'; import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html'; +import makeDashboardPublicDialogTemplate from './make-dashboard-public-dialog.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export function DashboardCardController($scope, types, customerService) { +export function MakeDashboardPublicDialogController($mdDialog, $translate, toast, dashboardService, dashboard) { + + var vm = this; + + vm.dashboard = dashboard; + vm.publicLink = dashboardService.getPublicDashboardLink(dashboard); + + vm.onPublicLinkCopied = onPublicLinkCopied; + vm.close = close; + + function onPublicLinkCopied(){ + toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element('#make-dialog-public-content'), 'bottom left'); + } + + function close() { + $mdDialog.hide(); + } + +} + +/*@ngInject*/ +export function DashboardCardController(types) { var vm = this; @@ -31,27 +53,22 @@ export function DashboardCardController($scope, types, customerService) { vm.isAssignedToCustomer = function() { if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' && - vm.item.customerId.id != vm.types.id.nullUid) { + vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { return true; } return false; } - $scope.$watch('vm.item', - function() { - if (vm.isAssignedToCustomer()) { - customerService.getCustomerTitle(vm.item.customerId.id).then( - function success(title) { - vm.customerTitle = title; - } - ); - } + vm.isPublic = function() { + if (vm.item && vm.item.assignedCustomer && vm.parentCtl.dashboardsScope === 'tenant' && vm.item.assignedCustomer.isPublic) { + return true; } - ); + return false; + } } /*@ngInject*/ -export function DashboardsController(userService, dashboardService, customerService, importExport, types, $scope, $controller, +export function DashboardsController(userService, dashboardService, customerService, importExport, types, $state, $stateParams, $mdDialog, $document, $q, $translate) { var customerId = $stateParams.customerId; @@ -119,6 +136,7 @@ export function DashboardsController(userService, dashboardService, customerServ vm.dashboardsScope = $state.$current.data.dashboardsType; vm.assignToCustomer = assignToCustomer; + vm.makePublic = makePublic; vm.unassignFromCustomer = unassignFromCustomer; vm.exportDashboard = exportDashboard; @@ -136,6 +154,17 @@ export function DashboardsController(userService, dashboardService, customerServ customerId = user.customerId; } + if (customerId) { + vm.customerDashboardsTitle = $translate.instant('customer.dashboards'); + customerService.getShortCustomerInfo(customerId).then( + function success(info) { + if (info.isPublic) { + vm.customerDashboardsTitle = $translate.instant('customer.public-dashboards'); + } + } + ); + } + if (vm.dashboardsScope === 'tenant') { fetchDashboardsFunction = function (pageLink) { return dashboardService.getTenantDashboards(pageLink); @@ -155,8 +184,21 @@ export function DashboardsController(userService, dashboardService, customerServ name: function() { $translate.instant('action.export') }, details: function() { return $translate.instant('dashboard.export') }, icon: "file_download" - }, - { + }); + + dashboardActionsList.push({ + onAction: function ($event, item) { + makePublic($event, item); + }, + name: function() { return $translate.instant('action.share') }, + details: function() { return $translate.instant('dashboard.make-public') }, + icon: "share", + isEnabled: function(dashboard) { + return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid); + } + }); + + dashboardActionsList.push({ onAction: function ($event, item) { assignToCustomer($event, [ item.id.id ]); }, @@ -166,19 +208,29 @@ export function DashboardsController(userService, dashboardService, customerServ isEnabled: function(dashboard) { return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid); } - }, - { + }); + dashboardActionsList.push({ onAction: function ($event, item) { - unassignFromCustomer($event, item); + unassignFromCustomer($event, item, false); }, name: function() { return $translate.instant('action.unassign') }, details: function() { return $translate.instant('dashboard.unassign-from-customer') }, icon: "assignment_return", isEnabled: function(dashboard) { - return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid; + return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && !dashboard.assignedCustomer.isPublic; } - } - ); + }); + dashboardActionsList.push({ + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('dashboard.make-private') }, + icon: "reply", + isEnabled: function(dashboard) { + return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && dashboard.assignedCustomer.isPublic; + } + }); dashboardActionsList.push( { @@ -262,11 +314,27 @@ export function DashboardsController(userService, dashboardService, customerServ dashboardActionsList.push( { onAction: function ($event, item) { - unassignFromCustomer($event, item); + unassignFromCustomer($event, item, false); }, name: function() { return $translate.instant('action.unassign') }, details: function() { return $translate.instant('dashboard.unassign-from-customer') }, - icon: "assignment_return" + icon: "assignment_return", + isEnabled: function(dashboard) { + return dashboard && !dashboard.assignedCustomer.isPublic; + } + } + ); + dashboardActionsList.push( + { + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('dashboard.make-private') }, + icon: "reply", + isEnabled: function(dashboard) { + return dashboard && dashboard.assignedCustomer.isPublic; + } } ); @@ -336,7 +404,28 @@ export function DashboardsController(userService, dashboardService, customerServ } function saveDashboard(dashboard) { - return dashboardService.saveDashboard(dashboard); + var deferred = $q.defer(); + dashboardService.saveDashboard(dashboard).then( + function success(savedDashboard) { + var dashboards = [ savedDashboard ]; + customerService.applyAssignedCustomersInfo(dashboards).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 assignToCustomer($event, dashboardIds) { @@ -418,15 +507,27 @@ export function DashboardsController(userService, dashboardService, customerServ assignToCustomer($event, dashboardIds); } - function unassignFromCustomer($event, dashboard) { + function unassignFromCustomer($event, dashboard, isPublic) { if ($event) { $event.stopPropagation(); } + var title; + var content; + var label; + if (isPublic) { + title = $translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title}); + content = $translate.instant('dashboard.make-private-dashboard-text'); + label = $translate.instant('dashboard.make-private-dashboard'); + } else { + title = $translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}); + content = $translate.instant('dashboard.unassign-dashboard-text'); + label = $translate.instant('dashboard.unassign-dashboard'); + } var confirm = $mdDialog.confirm() .targetEvent($event) - .title($translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title})) - .htmlContent($translate.instant('dashboard.unassign-dashboard-text')) - .ariaLabel($translate.instant('dashboard.unassign-dashboard')) + .title(title) + .htmlContent(content) + .ariaLabel(label) .cancel($translate.instant('action.no')) .ok($translate.instant('action.yes')); $mdDialog.show(confirm).then(function () { @@ -436,6 +537,25 @@ export function DashboardsController(userService, dashboardService, customerServ }); } + function makePublic($event, dashboard) { + if ($event) { + $event.stopPropagation(); + } + dashboardService.makeDashboardPublic(dashboard.id.id).then(function success(dashboard) { + $mdDialog.show({ + controller: 'MakeDashboardPublicDialogController', + controllerAs: 'vm', + templateUrl: makeDashboardPublicDialogTemplate, + locals: {dashboard: dashboard}, + parent: angular.element($document[0].body), + fullscreen: true, + targetEvent: $event + }).then(function () { + vm.grid.refreshList(); + }); + }); + } + function exportDashboard($event, dashboard) { $event.stopPropagation(); importExport.exportDashboard(dashboard.id.id); diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html index bae57b91d0..dde2f864b7 100644 --- a/ui/src/app/dashboard/dashboards.tpl.html +++ b/ui/src/app/dashboard/dashboards.tpl.html @@ -24,7 +24,8 @@ dashboard-scope="vm.dashboardsScope" the-form="vm.grid.detailsForm" on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" - on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)" + on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)" + on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)" on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)" on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js index 1c2e461a96..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); @@ -37,6 +37,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ scope.widgetConfig = scope.widget.config; var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; + scope.isDataEnabled = !widgetInfo.useCustomDatasources; if (!settingsSchema || settingsSchema === '') { scope.settingsSchema = {}; } else { @@ -57,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 7aa63395f9..279d3113a8 100644 --- a/ui/src/app/dashboard/edit-widget.tpl.html +++ b/ui/src/app/dashboard/edit-widget.tpl.html @@ -18,11 +18,12 @@
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js index dc74c76ed4..c8ba734a75 100644 --- a/ui/src/app/dashboard/index.js +++ b/ui/src/app/dashboard/index.js @@ -24,28 +24,25 @@ 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'; import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive'; import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive'; +import thingsboardSocialsharePanel from '../components/socialshare-panel.directive'; import thingsboardTypes from '../common/types.constant'; import thingsboardItemBuffer from '../services/item-buffer.service'; import thingsboardImportExport from '../import-export'; import DashboardRoutes from './dashboard.routes'; -import {DashboardsController, DashboardCardController} from './dashboards.controller'; +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, @@ -59,24 +56,22 @@ export default angular.module('thingsboard.dashboard', [ thingsboardApiDashboard, thingsboardApiCustomer, thingsboardDetailsSidenav, - thingsboardDeviceFilter, thingsboardWidgetConfig, thingsboardDashboardSelect, thingsboardDashboard, thingsboardExpandFullscreen, - thingsboardWidgetsBundleSelect + thingsboardWidgetsBundleSelect, + thingsboardSocialsharePanel ]) .config(DashboardRoutes) .controller('DashboardsController', DashboardsController) .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/dashboard/make-dashboard-public-dialog.tpl.html b/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html new file mode 100644 index 0000000000..8d96b30225 --- /dev/null +++ b/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html @@ -0,0 +1,62 @@ + + +
+ +
+

+ + + + +
+
+ +
+ + +
+
{{ vm.publicLink }}
+ + + + {{ 'dashboard.copy-public-link' | translate }} + + +
+
dashboard.public-dashboard-notice
+ + +
+
+
+ + + {{ 'action.ok' | + translate }} + + +
+
diff --git a/ui/src/app/device/add-devices-to-customer.controller.js b/ui/src/app/device/add-devices-to-customer.controller.js index 9fd0cec6fd..c54daab3a3 100644 --- a/ui/src/app/device/add-devices-to-customer.controller.js +++ b/ui/src/app/device/add-devices-to-customer.controller.js @@ -52,7 +52,7 @@ export default function AddDevicesToCustomerController(deviceService, $mdDialog, fetchMoreItems_: function () { if (vm.devices.hasNext && !vm.devices.pending) { vm.devices.pending = true; - deviceService.getTenantDevices(vm.devices.nextPageLink).then( + deviceService.getTenantDevices(vm.devices.nextPageLink, false).then( function success(devices) { vm.devices.data = vm.devices.data.concat(devices.data); vm.devices.nextPageLink = devices.nextPageLink; diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js index 560a4dc10f..701fd37754 100644 --- a/ui/src/app/device/attribute/attribute-table.directive.js +++ b/ui/src/app/device/attribute/attribute-table.directive.js @@ -72,7 +72,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS scope.$watch("deviceId", function(newVal, prevVal) { if (newVal && !angular.equals(newVal, prevVal)) { scope.resetFilter(); - scope.getDeviceAttributes(); + scope.getDeviceAttributes(false, true); } }); @@ -81,7 +81,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS scope.mode = 'default'; scope.query.search = null; scope.selectedAttributes = []; - scope.getDeviceAttributes(); + scope.getDeviceAttributes(false, true); } }); @@ -117,15 +117,25 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS } } - scope.getDeviceAttributes = function(forceUpdate) { + scope.onReorder = function() { + scope.getDeviceAttributes(false, false); + } + + scope.onPaginate = function() { + scope.getDeviceAttributes(false, false); + } + + scope.getDeviceAttributes = function(forceUpdate, reset) { if (scope.attributesDeferred) { scope.attributesDeferred.resolve(); } if (scope.deviceId && scope.attributeScope) { - scope.attributes = { - count: 0, - data: [] - }; + if (reset) { + scope.attributes = { + count: 0, + data: [] + }; + } scope.checkSubscription(); scope.attributesDeferred = deviceService.getDeviceAttributes(scope.deviceId, scope.attributeScope.value, scope.query, function(attributes, update, apply) { diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html index 915fbea861..b6099c4ffa 100644 --- a/ui/src/app/device/attribute/attribute-table.tpl.html +++ b/ui/src/app/device/attribute/attribute-table.tpl.html @@ -126,7 +126,7 @@ - + @@ -147,7 +147,7 @@ + md-on-paginate="onPaginate" md-page-select>
Last update time Key
+ + + + + + + + + + + + + + +
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-es.js b/ui/src/app/locale/locale.constant-es.js new file mode 100644 index 0000000000..153019d44c --- /dev/null +++ b/ui/src/app/locale/locale.constant-es.js @@ -0,0 +1,818 @@ +/* + * 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 function addLocaleSpanish(locales) { + var es_ES = { + "access": { + "unauthorized": "No autorizado", + "unauthorized-access": "Acceso no autorizado", + "unauthorized-access-text": "Debes iniciar sesión para tener acceso a este recurso!", + "access-forbidden": "Acceso Prohibido", + "access-forbidden-text": "No tienes derechos para acceder a esta ubicación!
Intenta iniciar sesión con otro usuario si todavía quieres acceder a esta ubicación.", + "refresh-token-expired": "La sesión ha expirado", + "refresh-token-failed": "No se puede actualizar la sesión" + }, + "action": { + "activate": "Activar", + "suspend": "Suspender", + "save": "Guardar", + "saveAs": "Guardar como", + "cancel": "Cancelar", + "ok": "OK", + "delete": "Borrar", + "add": "Agregar", + "yes": "Si", + "no": "No", + "update": "Actualizar", + "remove": "Eliminar", + "search": "Buscar", + "assign": "Asignar", + "unassign": "Cancelar asignación", + "share": "Compartir", + "make-private": "Hacer privado", + "apply": "Aplicar", + "apply-changes": "Aplicar cambios", + "edit-mode": "Modo Edición", + "enter-edit-mode": "Modo Edición", + "decline-changes": "Descartar cambios", + "close": "Cerrar", + "back": "Atrás", + "run": "Correr", + "sign-in": "Regístrate!", + "edit": "Editar", + "view": "Ver", + "create": "Crear", + "drag": "Arrastrar", + "refresh": "Refrescar", + "undo": "Deshacer", + "copy": "Copiar", + "paste": "Pegar", + "import": "Importar", + "export": "Exportar", + "share-via": "Compartir vía {{provider}}" + }, + "aggregation": { + "aggregation": "Agregación", + "function": "Función de Agregación", + "limit": "Valores Max", + "group-interval": "Intervalo de agrupación", + "min": "Min", + "max": "Max", + "avg": "Promedio", + "sum": "Suma", + "count": "Cuenta", + "none": "Ninguno" + }, + "admin": { + "general": "General", + "general-settings": "Ajustes General", + "outgoing-mail": "Mail de Salida", + "outgoing-mail-settings": "Ajustes del Mail de Salida", + "system-settings": "Sistema", + "test-mail-sent": "Mail de prueba enviado correctamente!", + "base-url": "URL Base", + "base-url-required": "URL Base requerida.", + "mail-from": "Mail Desde", + "mail-from-required": "Mail Desde requerido.", + "smtp-protocol": "Protocolo SMTP", + "smtp-host": "Host SMTP", + "smtp-host-required": "Host SMTP requerido.", + "smtp-port": "Puerto SMTP", + "smtp-port-required": "Debe ingresar un Puerto SMTP.", + "smtp-port-invalid": "No parece un Puerto SMTP valido.", + "timeout-msec": "Timeout (ms)", + "timeout-required": "Timeout requerido.", + "timeout-invalid": "No parece un Timeout valido.", + "enable-tls": "Habilitar TLS", + "send-test-mail": "Enviar mail de prueba" + }, + "attribute": { + "attributes": "Atributos", + "latest-telemetry": "Última telemetría", + "attributes-scope": "Alcance de los atributos del dispositivo", + "scope-latest-telemetry": "Última telemetría", + "scope-client": "Atributos del Cliente", + "scope-server": "Atributos del Servidor", + "scope-shared": "Atributos Compartidos", + "add": "Agregar atributo", + "key": "Clave", + "key-required": "Clave del atributo requerida.", + "value": "Valor", + "value-required": "Valor del atributo requerido.", + "delete-attributes-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 atributo} other {# atributos} }?", + "delete-attributes-text": "Ten cuidado, luego de confirmar el atributo será eliminado, y la información relacionada será irrecuperable.", + "delete-attributes": "Borrar atributo", + "enter-attribute-value": "Ingresar valor del atributo", + "show-on-widget": "Mostrar en Widget", + "widget-mode": "Widget", + "next-widget": "Widget siguiente", + "prev-widget": "Widget anterior", + "add-to-dashboard": "Agregar al Panel", + "add-widget-to-dashboard": "Agregar widget al Panel", + "selected-attributes": "{ count, select, 1 {1 atributo} other {# atributos} } seleccionados", + "selected-telemetry": "{ count, select, 1 {1 unidad de telemetría } other {# unidades de telemetría} } seleccionadas." + }, + "confirm-on-exit": { + "message": "Tienes cambios sin guardar. ¿Estás seguro que quieres abandonar la página?", + "html-message": "Tienes cambios sin guardar.
¿Estás seguro que quieres abandonar la página?", + "title": "Cambios sin guardar" + }, + "contact": { + "country": "País", + "city": "Ciudad", + "state": "Estado/Provincia", + "postal-code": "Código Postal", + "postal-code-invalid": "Solo se permiten dígitos.", + "address": "Dirección", + "address2": "Dirección 2", + "phone": "Teléfono", + "email": "Email", + "no-address": "Sin Dirección" + }, + "common": { + "username": "Usuario", + "password": "Contraseña", + "enter-username": "Ingresa el nombre de usuario.", + "enter-password": "Ingresa la contraseña", + "enter-search": "Ingresa búsqueda" + }, + "customer": { + "customers": "Clientes", + "management": "Gestión de Clientes", + "dashboard": "Panel del Cliente", + "dashboards": "Paneles del Cliente", + "devices": "Panel del Cliente", + "public-dashboards": "Paneles Públicos", + "public-devices": "Dispositivos Públicos", + "add": "Agregar cliente", + "delete": "Borrar cliente", + "manage-customer-users": "Gestionar usuarios del cliente", + "manage-customer-devices": "Gestionar dispositivos del cliente", + "manage-customer-dashboards": "Gestionar paneles del cliente", + "manage-public-devices": "Gestionar dispositivos públicos", + "manage-public-dashboards": "Gestionar paneles públicos", + "add-customer-text": "Agregar nuevo cliente", + "no-customers-text": "No se encontrar clientes", + "customer-details": "Detalles del cliente", + "delete-customer-title": "¿Estás seguro que quieres eliminar el cliente '{{customerTitle}}'?", + "delete-customer-text": "Ten cuidado, luego de confirmar el cliente será eliminado y toda la información relacionada será irrecuperable.", + "delete-customers-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 cliente} other {# clientes} }?", + "delete-customers-action-title": "Borrar { count, select, 1 {1 cliente} other {# clientes} }", + "delete-customers-text": "Ten cuidado, luego de confirmar todos los clientes seleccionados serán eliminados y su información relacionada será irrecuperable.", + "manage-users": "Gestionar usuarios", + "manage-devices": "Gestionar dispositivos", + "manage-dashboards": "Gestionar paneles", + "title": "Título", + "title-required": "Título requerido.", + "description": "Descripción" + }, + "datetime": { + "date-from": "Fecha desde", + "time-from": "Tiempo desde", + "date-to": "Fecha hasta", + "time-to": "Tiempo hasta" + }, + "dashboard": { + "dashboard": "Panel", + "dashboards": "Paneles", + "management": "Gestión de Paneles", + "view-dashboards": "Ver paneles", + "add": "Agregar Panel", + "assign-dashboard-to-customer": "Asignar panel(es) a cliente", + "assign-dashboard-to-customer-text": "Por favor, seleccione algún panel para asignar al Cliente.", + "assign-to-customer-text": "Por favor, seleccione algún cliente para asignar al(los) panel(es).", + "assign-to-customer": "Asignar a cliente", + "unassign-from-customer": "Desasignar del cliente", + "make-public": "Hacer panel público", + "make-private": "Hacer panel privado", + "no-dashboards-text": "Ningún panel encontrado", + "no-widgets": "Ningún widget configurado", + "add-widget": "Agregar nuevo widget", + "title": "Titulo", + "select-widget-title": "Seleccionar widget", + "select-widget-subtitle": "Lista de tipos de widgets", + "delete": "Eliminar panel", + "title-required": "Título requerido.", + "description": "Descripción", + "details": "Detalles", + "dashboard-details": "Detalles del panel", + "add-dashboard-text": "Agregar nuevo panel", + "assign-dashboards": "Asignar paneles", + "assign-new-dashboard": "Asignar nuevo panel", + "assign-dashboards-text": "Asignar { count, select, 1 {1 panel} other {# paneles} } al cliente", + "delete-dashboards": "Eliminar paneles", + "unassign-dashboards": "Desasignar paneles", + "unassign-dashboards-action-title": "Desasignar { count, select, 1 {1 paneles} other {# paneles} } del cliente", + "delete-dashboard-title": "¿Estás seguro que quieres eliminar el panel '{{dashboardTitle}}'?", + "delete-dashboard-text": "Ten cuidado, el panel seleccionado será eliminado y la información relacionada sera irrecuperable.", + "delete-dashboards-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 panel} other {# paneles} }?", + "delete-dashboards-action-title": "Eliminar { count, select, 1 {1 panel} other {# paneles} }", + "delete-dashboards-text": "Ten cuidado, los paneles seleccionados serán eliminados y la información relacionada será irrecuperable.", + "unassign-dashboard-title": "¿Estás seguro que quieres desasignar el panel '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Luego de confirmar, el panel será desasignado y no podrá ser accesible por el cliente.", + "unassign-dashboard": "Desasignar panel", + "unassign-dashboards-title": "¿Estás seguro que quieres desasignar { count, select, 1 {1 panel} other {# paneles} }?", + "unassign-dashboards-text": "Luego de confirmar, los paneles seleccionados serán desasignados y no podrán ser accesibles por el cliente.", + "public-dashboard-title": "El panel ahora es público", + "public-dashboard-text": "Tu panel {{dashboardTitle}} es ahora público y podrá ser accedido desde: aquí:", + "public-dashboard-notice": "Nota: No olvides hacer públicos los dispositivos relacionados para acceder a sus datos.", + "make-private-dashboard-title": "¿Estás seguro que quieres hacer el panel '{{dashboardTitle}}' privado?", + "make-private-dashboard-text": "Luego de confirmar, el panel será privado y no podrá ser accesible por otros.", + "make-private-dashboard": "Hacer panel privado", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Seleccionar panel", + "no-dashboards-matching": "Panel '{{dashboard}}' no encontrado.", + "dashboard-required": "Panel requerido.", + "select-existing": "Seleccionar paneles existentes", + "create-new": "Crear nuevo panel", + "new-dashboard-title": "Nuevo título", + "open-dashboard": "Abrir panel", + "set-background": "Definir fondo", + "background-color": "Color de fondo", + "background-image": "Imagen de fondo", + "background-size-mode": "Modo tamaño de fondo", + "no-image": "No se ha seleccionado ningúna imagen", + "drop-image": "Suelte una imagen o haga clic para seleccionar un archivo para cargar.", + "settings": "Ajustes", + "columns-count": "Número de columnas", + "columns-count-required": "Número de columnas requerido.", + "min-columns-count-message": "Solo se permite un número mínimo de 10 columnas.", + "max-columns-count-message": "Solo se permite un número máximo de 1000 columnas.", + "widgets-margins": "Margen entre widgets", + "horizontal-margin": "Margen horizontal", + "horizontal-margin-required": "Margen horizontal requerido.", + "min-horizontal-margin-message": "Solo se permite margen horizontal mínimo de 0.", + "max-horizontal-margin-message": "Solo se permite margen horizontal máximo de 50.", + "vertical-margin": "Margen vertical", + "vertical-margin-required": "Margen vertical requerido.", + "min-vertical-margin-message": "Solo se permite margen vertical mínimo de 0.", + "max-vertical-margin-message": "Solo se permite margen vertical máximo de 50.", + "display-title": "Mostrar título del panel", + "title-color": "Color del título", + "display-device-selection": "Mostrar selección de dispositivo", + "display-dashboard-timewindow": "Mostrar ventana de tiempo", + "display-dashboard-export": "Mostrar exportar", + "import": "Importar panel", + "export": "Exportar panel", + "export-failed-error": "Imposible exportar panel: {{error}}", + "create-new-dashboard": "Crear nuevo panel", + "dashboard-file": "Archivo del panel", + "invalid-dashboard-file-error": "Imposible importar panel: Estructura de datos inválida.", + "dashboard-import-missing-aliases-title": "Configurar alias utilizados por el panel importado", + "create-new-widget": "Crear nuevo widget", + "import-widget": "Importar widget", + "widget-file": "Archivo de widget", + "invalid-widget-file-error": "Imposible importar widget: Estructura de datos inválida.", + "widget-import-missing-aliases-title": "Configurar alias utilizados por el widget", + "open-toolbar": "Abrir toolbar del panel", + "close-toolbar": "Cerrar toolbar", + "configuration-error": "Error de configuración", + "alias-resolution-error-title": "Error de configuración de alias del panel", + "invalid-aliases-config": "No se puede encontrar ningún dispositivo que coincida con algunos de los alias de filtro.
" + + "Póngase en contacto con su administrador para resolver este problema.", + "select-devices": "Seleccionar dispositivos", + "assignedToCustomer": "Asignado al cliente", + "public": "Público", + "public-link": "Link público", + "copy-public-link": "Copiar link público", + "public-link-copied-message": "El link público del panel se ha copiado al portapapeles" + }, + "datakey": { + "settings": "Ajustes", + "advanced": "Avanzado", + "label": "Etiqueta", + "color": "Color", + "data-generation-func": "Función de generación de datos", + "use-data-post-processing-func": "Usar funcíon de post-procesamiendo de datos", + "configuration": "Ajustes de clave de datos", + "timeseries": "Serie de tiempos", + "attributes": "Atributos", + "timeseries-required": "Series de tiempo del dispositivo requerido.", + "timeseries-or-attributes-required": "Series de tiempo/Atributos requeridos.", + "function-types": "Tipos de funciones", + "function-types-required": "Tipos de funciones requerido." + }, + "datasource": { + "type": "Típo de fuente de datos", + "add-datasource-prompt": "Por favor, agrega una fuente de datos" + }, + "details": { + "edit-mode": "Modo Edición", + "toggle-edit-mode": "Ir a Modo Edición" + }, + "device": { + "device": "Dispositivo", + "device-required": "Dispositivo requerido.", + "devices": "Dispositivos", + "management": "Gestión de Dispositivos", + "view-devices": "Ver dispositivos", + "device-alias": "Alias de dispositivo", + "aliases": "Alias de dispositivos", + "no-alias-matching": "'{{alias}}' no encontrado.", + "no-aliases-found": "Ningún alias encontrado.", + "no-key-matching": "'{{key}}' no encontrado.", + "no-keys-found": "Ninguna clave encontrada.", + "create-new-alias": "Crear nuevo alias!", + "create-new-key": "Crear nueva clave!", + "duplicate-alias-error": "Alias duplicado '{{alias}}'.
El alias de los dispositivos deben ser únicos dentro del panel.", + "configure-alias": "Configurar alias '{{alias}}'", + "no-devices-matching": "No se encontró dispositivo '{{device}}'", + "alias": "Alias", + "alias-required": "Alias de dispositivo requerido.", + "remove-alias": "Eliminar alias", + "add-alias": "Agregar alias", + "name-starts-with": "Nombre empieza con", + "device-list": "Lista de dispositivos", + "use-device-name-filter": "Usar filtro", + "device-list-empty": "Ningún dispositivo seleccionado.", + "device-name-filter-required": "Nombre de filtro requerido.", + "device-name-filter-no-device-matched": "Ningún dispositivo encontrado que comience con '{{device}}'.", + "add": "Agregar dispositivo", + "assign-to-customer": "Asignar a cliente", + "assign-device-to-customer": "Asignar dispositivo(s) a Cliente", + "assign-device-to-customer-text": "Por favor, seleccione los dispositivos que serán asignados al cliente", + "make-public": "Hacer dispositivo público", + "make-private": "Hacer dispositivo privado", + "no-devices-text": "Ningún dispositivo encontrado", + "assign-to-customer-text": "Por favor, seleccione el cliente para asignar el(los) dispositivo(s)", + "device-details": "Detalles del dispositivo", + "add-device-text": "Agregar nuevo dispositivo", + "credentials": "Credenciales", + "manage-credentials": "Gestionar credenciales", + "delete": "Eliminar dispositivo", + "assign-devices": "Asignar dispositivo", + "assign-devices-text": "Asignar { count, select, 1 {1 dispositivo} other {# dispositivos} } al cliente", + "delete-devices": "Eliminar dispositivo", + "unassign-from-customer": "Desasignar del cliente", + "unassign-devices": "Desasignar dispositivos", + "unassign-devices-action-title": "Desasignar { count, select, 1 {1 dispositivo} other {# dispositivos} } del cliente", + "assign-new-device": "Asignar nuevo dispositivo", + "make-public-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' público?", + "make-public-device-text": "Luego de confirmar, el dispositivo y la información relacionada serán públicos y podrá ser accesible por otros.", + "make-private-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' privado?", + "make-private-device-text": "Luego de confirmar, el dispositivo y la información relacionada serán privados y no podrá ser accesible por otros.", + "view-credentials": "Ver credenciales", + "delete-device-title": "¿Estás seguro que quieres eliminar el dispositivo '{{deviceName}}'?", + "delete-device-text": "Ten cuidado, luego de confirmar los dispositivos serán eliminados y la información relacionada será irrecuperable.", + "delete-devices-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 dispositivo} other {# dispositivos} }?", + "delete-devices-action-title": "Eliminar { count, select, 1 {1 dispositivo} other {# dispositivos} }", + "delete-devices-text": "Ten cuidado, luego de confirmar los dispositivos seleccionados serán eliminados y la información relacionada será irrecuperable.", + "unassign-device-title": "¿Estás seguro que quieres desasignar el dispositivo '{{deviceName}}'?", + "unassign-device-text": "Luego de confirmar el dispositivo será desasignado y no podrá ser accesible por el cliente.", + "unassign-device": "Desasignar dispositivo", + "unassign-devices-title": "¿Estás seguro que quieres desasignar { count, select, 1 {1 dispositivo} other {# dispositivos} }?", + "unassign-devices-text": "Luego de confirmar los dispositivos seleccionados serán desasignados y no podrán ser accedidos por el cliente.", + "device-credentials": "Credenciales del dispositivo", + "credentials-type": "Tipo de credencial", + "access-token": "Access token", + "access-token-required": "Access token requerido.", + "access-token-invalid": "Access token debe tener entre 1 a 20 caracteres.", + "rsa-key": "Clave pública RSA", + "rsa-key-required": "Clave pública RSA requerida.", + "secret": "Secreta", + "secret-required": "Secreta requerida.", + "name": "Nombre", + "name-required": "Nombre requerido.", + "description": "Descripción", + "events": "Eventos", + "details": "Detalles", + "copyId": "Copiar ID", + "copyAccessToken": "Copiar access token", + "idCopiedMessage": "Id del dispositivo copiado al portapapeles", + "accessTokenCopiedMessage": "Access token del dispositivo copiado al portapapeles", + "assignedToCustomer": "Asignado al cliente", + "unable-delete-device-alias-title": "Imposible eliminar alias del dispositivo", + "unable-delete-device-alias-text": "Alias '{{deviceAlias}}' no puede ser eliminado. Esta siendo usado por el(los) widget(s):
{{widgetsList}}", + "is-gateway": "Es gateway", + "public": "Público", + "device-public": "Dispositivo público" + }, + "dialog": { + "close": "Cerrar cuadro de diálogo" + }, + "error": { + "unable-to-connect": "Imposible conectar con el servidor! Por favor, revise su conexión a internet.", + "unhandled-error-code": "Código de error no manejado: {{errorCode}}", + "unknown-error": "Error desconocido" + }, + "event": { + "event-type": "Tipo de evento", + "type-alarm": "Alarma", + "type-error": "Error", + "type-lc-event": "Ciclo de vida", + "type-stats": "Estadísticas", + "no-events-prompt": "Ningún evento encontrado.", + "error": "Error", + "alarm": "Alarma", + "event-time": "Hora del evento", + "server": "Servidor", + "body": "Cuerpo", + "method": "Método", + "event": "Evento", + "status": "Status", + "success": "Éxito", + "failed": "Fallo", + "messages-processed": "Mensajes procesados", + "errors-occurred": "Ocurrieron errores" + }, + "fullscreen": { + "expand": "Expandir a Pantalla Completa", + "exit": "Salir de Pantalla Completa", + "toggle": "Cambiar el modo de Pantalla Completa", + "fullscreen": "Pantalla Completa" + }, + "function": { + "function": "Función" + }, + "grid": { + "delete-item-title": "¿Estás seguro que quieres eliminar este item?", + "delete-item-text": "Ten cuidado, luego de confirmar el item será eliminado y la información relacionada será irrecuperable.", + "delete-items-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 item} other {# items} }?", + "delete-items-action-title": "Eliminar { count, select, 1 {1 item} other {# items} }", + "delete-items-text": "Ten cuidado, luego de confirmar los items seleccionados serán eliminados y la información relacionada será irrecuperable.", + "add-item-text": "Agregar nuevo item", + "no-items-text": "Ningún item encontrado", + "item-details": "Detalles del item", + "delete-item": "Borrar Item", + "delete-items": "Borrar Items", + "scroll-to-top": "Ir hacia arriba" + }, + "help": { + "goto-help-page": "Ir a Página de Ayuda" + }, + "home": { + "home": "Principal", + "profile": "Perfil", + "logout": "Salir", + "menu": "Menu", + "avatar": "Avatar", + "open-user-menu": "Abrir menú de usuario" + }, + "import": { + "no-file": "Ningún archivo seleccionado", + "drop-file": "Arrastra un archivo JSON o clickea para seleccionar uno." + }, + "item": { + "selected": "Seleccionado" + }, + "js-func": { + "no-return-error": "La función debe retornar un valor!", + "return-type-mismatch": "La función debe retornar un valor de tipo: '{{type}}'!" + }, + "legend": { + "position": "Posición de leyenda", + "show-max": "Mostrar máximo", + "show-min": "Mostrar mínimo", + "show-avg": "Mostrar promedio", + "show-total": "Mostrar total", + "settings": "Ajustes de leyenda.", + "min": "min", + "max": "max", + "avg": "prom", + "total": "total" + }, + "login": { + "login": "Ingresar", + "request-password-reset": "Pedir restablecer contraseña", + "reset-password": "Restablecer contraseña", + "create-password": "Crear contraseña", + "passwords-mismatch-error": "Las contraseñas deben ser las mismas!", + "password-again": "Reingresa la contraseña", + "sign-in": "Iniciar sesión", + "username": "Usuario (email)", + "remember-me": "Recordar", + "forgot-password": "¿Olvidaste tu contraseña?", + "password-reset": "Restablecer Contraseña", + "new-password": "Nueva contraseña", + "new-password-again": "Repita la nueva contraseña", + "password-link-sent-message": "Se ha enviado el enlace de restablecimiento de contraseña con éxito!", + "email": "Email" + }, + "plugin": { + "plugins": "Plugins", + "delete": "Eliminar plugin", + "activate": "Activar plugin", + "suspend": "Suspender plugin", + "active": "Activo", + "suspended": "Suspendido", + "name": "Nombre", + "name-required": "Nombre requerido.", + "description": "Descripción", + "add": "Agregar Plugin", + "delete-plugin-title": "¿Estás seguro que quieres eliminar el plugin '{{pluginName}}'?", + "delete-plugin-text": "Ten cuidado, luego de confirmar el plugin será eliminado y la información relacionada será irrecuperable.", + "delete-plugins-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 plugin} other {# plugins} }?", + "delete-plugins-action-title": "Eliminar { count, select, 1 {1 plugin} other {# plugins} }", + "delete-plugins-text": "Ten cuidado, luego de confirmar todos los plugins seleccionados serán eliminados y la información relacionada será irrecuperable.", + "add-plugin-text": "Agregar nuevo plugin", + "no-plugins-text": "Ningún plugin encontrado", + "plugin-details": "Detalles", + "api-token": "API token", + "api-token-required": "API token requerido.", + "type": "Tipo del plugin", + "type-required": "Tipo requerido.", + "configuration": "Ajustes del plugin", + "system": "Sistema", + "select-plugin": "plugin", + "plugin": "Plugin", + "no-plugins-matching": "No se encontraron plugins: '{{plugin}}'", + "plugin-required": "Plugin requerido.", + "plugin-require-match": "Por favor, elija un plugin existente.", + "events": "Eventos", + "details": "Detalles", + "import": "Importar plugin", + "export": "Exportar plugin", + "export-failed-error": "Imposible exportar plugin: {{error}}", + "create-new-plugin": "Crear nuevo plugin", + "plugin-file": "Archivo", + "invalid-plugin-file-error": "Imposible de importar plugin: Estructura de datos inválida." + }, + "position": { + "top": "Arriba", + "bottom": "Abajo", + "left": "Izquierda", + "right": "Derecha" + }, + "profile": { + "profile": "Perfil", + "change-password": "Cambiar contraseña", + "current-password": "Contraseña actual" + }, + "rule": { + "rules": "Reglas", + "delete": "Eliminar regla", + "activate": "Activar regla", + "suspend": "Suspender regla", + "active": "Activada", + "suspended": "Suspendida", + "name": "Nombre", + "name-required": "Nombre requerido.", + "description": "Descripción", + "add": "Agregar Regla", + "delete-rule-title": "¿Estás seguro que quieres eliminar la regla '{{ruleName}}'?", + "delete-rule-text": "Ten cuidado, luego de confirmar la regla será eliminada y la información relacionada será irrecuperable.", + "delete-rules-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 regla} other {# reglas} }?", + "delete-rules-action-title": "Eliminar { count, select, 1 {1 regla} other {# reglas} }", + "delete-rules-text": "Ten cuidado, luego de confirmar todas las reglas seleccionadas serán borradas y la información relacionada será irrecuperable.", + "add-rule-text": "Agregar nueva regla", + "no-rules-text": "Ninguna regla encontrada", + "rule-details": "Detalles", + "filters": "Filtros", + "filter": "Filtro", + "add-filter-prompt": "Por favor, ingresa un filtro", + "remove-filter": "Eliminar filtro", + "add-filter": "Agregar filtro", + "filter-name": "Nombre", + "filter-type": "Tipo", + "edit-filter": "Editar filtro", + "view-filter": "Ver filtro", + "component-name": "Nombre", + "component-name-required": "Nombre requerido.", + "component-type": "Tipo", + "component-type-required": "Tipo requerido.", + "processor": "Procesador", + "no-processor-configured": "Ningún procesador encontrado", + "create-processor": "Crear procesador", + "processor-name": "Nombre", + "processor-type": "Tipo", + "plugin-action": "Acción del Plugin", + "action-name": "Nombre", + "action-type": "Tipo", + "create-action-prompt": "Por favor, crea una acción.", + "create-action": "Crear acción", + "details": "Detalles", + "events": "Eventos", + "system": "Sistema", + "import": "Importar regla", + "export": "Exportar regla", + "export-failed-error": "Imposible de exportar regla: {{error}}", + "create-new-rule": "Crear nueva regla", + "rule-file": "Archivo", + "invalid-rule-file-error": "Imposible de importar regla: Estructura de datos inválida." + }, + "rule-plugin": { + "management": "Gestión de Reglas y Plugins" + }, + "tenant": { + "tenants": "Tenants", + "management": "Gestión de Tenant", + "add": "Agregar Tenant", + "admins": "Admins", + "manage-tenant-admins": "Gestionar administradores tenant", + "delete": "Eliminar tenant", + "add-tenant-text": "Agregar nuevo tenant", + "no-tenants-text": "Ningún tenant encontrado", + "tenant-details": "Detalles del Tenant", + "delete-tenant-title": "¿Estás seguro que quieres eliminar el tenant '{{tenantTitle}}'?", + "delete-tenant-text": "Ten cuidado, luego de confirmar el tenant será eliminado y la información relacionada será irrecuperable.", + "delete-tenants-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 tenant} other {# tenants} }?", + "delete-tenants-action-title": "Eliminar { count, select, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "Ten cuidado, luego de confirmar los tenants seleccionados serán eliminados y la información relacionada será irrecuperable.", + "title": "Título", + "title-required": "Título requerido.", + "description": "Descripción" + }, + "timeinterval": { + "seconds-interval": "{ seconds, select, 1 {1 segundo} other {# segundos} }", + "minutes-interval": "{ minutes, select, 1 {1 minuto} other {# minutos} }", + "hours-interval": "{ hours, select, 1 {1 hora} other {# horas} }", + "days-interval": "{ days, select, 1 {1 día} other {# días} }", + "days": "Días", + "hours": "Horas", + "minutes": "Minutos", + "seconds": "Segundos", + "advanced": "Avanzado" + }, + "timewindow": { + "days": "{ days, select, 1 { día } other {# días } }", + "hours": "{ hours, select, 0 { horas } 1 {1 hora } other {# horas } }", + "minutes": "{ minutes, select, 0 { minutos } 1 {1 minuto } other {# minutos } }", + "seconds": "{ seconds, select, 0 { segundos } 1 {1 segundo } other {# segundos } }", + "realtime": "Tiempo-real", + "history": "Histórico", + "last-prefix": "último", + "period": "desde {{ startTime }} hasta {{ endTime }}", + "edit": "Editar ventana de tiempo", + "date-range": "Rango de fechas", + "last": "Últimos", + "time-period": "Período de tiempo" + }, + "user": { + "users": "Usuarios", + "customer-users": "Usuarios del Cliente", + "tenant-admins": "Tenant Admins", + "sys-admin": "Administrador del Sistema", + "tenant-admin": "Administrador Tenant", + "customer": "Cliente", + "anonymous": "Anónimo", + "add": "Agregar usuario", + "delete": "Eliminar usuario", + "add-user-text": "Agregar nuevo usuario", + "no-users-text": "Ningún usuario encontrado", + "user-details": "Detalles del usuario", + "delete-user-title": "¿Estás seguro que quieres eliminar el usuario '{{userEmail}}'?", + "delete-user-text": "Ten cuidado, luego de confirmar el usuario seleccionado será eliminado y la información relacionada será irrecuperable.", + "delete-users-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 usuario} other {# usuarios} }?", + "delete-users-action-title": "Borrar { count, select, 1 {1 usuario} other {# usuarios} }", + "delete-users-text": "Ten cuidado, luego de confirmar los usuarios seleccionados serán eliminados y la información relacionada será irrecuperable.", + "activation-email-sent-message": "Mail de activación enviado con éxito!", + "resend-activation": "Reenviar activación", + "email": "Email", + "email-required": "Email requerido.", + "first-name": "Nombre", + "last-name": "Apellido", + "description": "Descripción", + "default-dashboard": "Panel por defecto", + "always-fullscreen": "Siempre en pantalla completa" + }, + "value": { + "type": "Tipo de valor", + "string": "Cadena de texto", + "string-value": "Valor de cadena de texto", + "integer": "Nro entero", + "integer-value": "Valor de nro entero", + "invalid-integer-value": "Valor inválido", + "double": "Nro decimal", + "double-value": "Valor nro decimal", + "boolean": "Booleano", + "boolean-value": "Valor booleano", + "false": "Falso", + "true": "Verdadero" + }, + "widget": { + "widget-library": "Bibloteca de Widgets", + "widget-bundle": "Paquetes de Widgets", + "select-widgets-bundle": "Seleccionar paquete de widgets", + "management": "Gestión de Widgets", + "editor": "Editor de widgets", + "widget-type-not-found": "Problema al cargar la configuración del widget.
Probablemente asociado\n El tipo de widget fue eliminado.", + "widget-type-load-error": "Widget no pudo ser cargado debido a estos errores:", + "remove": "Eliminar widget", + "edit": "Editar widget", + "remove-widget-title": "¿Estás seguro que quieres eliminar el widget '{{widgetTitle}}'?", + "remove-widget-text": "Luego de confirmar el widget será eliminado y toda la información relacionada será irrecuperable..", + "timeseries": "Series de tiempo", + "latest-values": "Últimos valores", + "rpc": "Widget de control", + "static": "Widget estático", + "select-widget-type": "Seleccionar tipo de widget", + "missing-widget-title-error": "El titulo del widget debe ser especificado!", + "widget-saved": "Widget guardado", + "unable-to-save-widget-error": "Imposible guardar widget! Tiene errores!", + "save": "Guardar widget", + "saveAs": "Guardar widget como", + "save-widget-type-as": "Guardar tipo de widget como", + "save-widget-type-as-text": "Por favor, ingrese un nuevo titulo y/o seleccione un paquete de destino.", + "toggle-fullscreen": "Cambiar a pantalla completa", + "run": "Correr widget", + "title": "Titulo", + "title-required": "Titulo requerido.", + "type": "Tipo", + "resources": "Recursos", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Eliminar recurso", + "add-resource": "Agregar recurso", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Esquema de configuración", + "datakey-settings-schema": "Esquema de configuración de clave de datos", + "javascript": "Javascript", + "remove-widget-type-title": "¿Estás seguro que quieres eliminar el tipo del widget '{{widgetName}}'?", + "remove-widget-type-text": "Luego de confirmar el tipo será eliminado y la información relacionada será irrecuperable.", + "remove-widget-type": "Eliminar tipo de widget.", + "add-widget-type": "Agregar nuevo tipo de widget", + "widget-type-load-failed-error": "Error al cargar el tipo de widget!", + "widget-template-load-failed-error": "Error al cargar el template del widget!", + "add": "Agregar Widget", + "undo": "Deshacer cambios", + "export": "Exportar widget" + }, + "widgets-bundle": { + "current": "Paquete actual", + "widgets-bundles": "Paquete de Widgets", + "add": "Agregar paquete de widgets", + "delete": "Eliminar paquete de widgets", + "title": "Título", + "title-required": "Título requerido.", + "add-widgets-bundle-text": "Agregar nuevo paquete de widgets", + "no-widgets-bundles-text": "Ningún paquete de widgets encontrado", + "empty": "Paquete de widgets vacío.", + "details": "Detalles", + "widgets-bundle-details": "Detalles del paquete de Widgets", + "delete-widgets-bundle-title": "¿Estás seguro que desea eliminar el paquete de widgets '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Ten cuidado, luego de confirmar todos los paquetes seleccionados serán eliminados y su información relacionada será irrecuperable.", + "delete-widgets-bundles-title": "¿Estás seguro que deseas eliminar { count, select, 1 {1 paquete de widgets} other {# paquetes de widgets} }?", + "delete-widgets-bundles-action-title": "Eliminar { count, select, 1 {1 paquete de widgets} other {# paquetes de widgets} }", + "delete-widgets-bundles-text": "Ten cuidado, luego de confirmar todos los paquetes seleccionados serán eliminados y la información relacionada será irrecuperable.", + "no-widgets-bundles-matching": "Ningún paquete '{{widgetsBundle}}' encontrado.", + "widgets-bundle-required": "Paquete de widget requerido.", + "system": "Sistema", + "import": "Importar paquete de widgets", + "export": "Exportar paquete de widgets", + "export-failed-error": "Imposible exportar paquete de widgets: {{error}}", + "create-new-widgets-bundle": "Crear nuevo paquete de widgets", + "widgets-bundle-file": "Archivo de paquete de widgets", + "invalid-widgets-bundle-file-error": "Imposible importar paquete de widgets: Estructura de datos inválida." + }, + "widget-config": { + "data": "Datos", + "settings": "Ajustes", + "advanced": "Avanzado", + "title": "Titulo", + "general-settings": "Ajustes generales", + "display-title": "Mostrar titulo", + "drop-shadow": "Sombra", + "enable-fullscreen": "Habilitar pantalla completa", + "background-color": "Color de fondo", + "text-color": "Color del texto", + "padding": "Relleno", + "title-style": "Estilo de título", + "mobile-mode-settings": "Ajustes mobile.", + "order": "Orden", + "height": "Altura", + "units": "Caracter especial a mostrar en el siguiente valor", + "decimals": "Números de dígitos después de la coma", + "timewindow": "Ventana de tiempo", + "use-dashboard-timewindow": "Usar ventana de tiempo del Panel", + "display-legend": "Mostrar leyenda", + "datasources": "Set de datos", + "datasource-type": "Tipo", + "datasource-parameters": "Parámetros", + "remove-datasource": "Eliminar set de datos", + "add-datasource": "Agregar set de datos", + "target-device": "Dispositivo destino" + }, + "widget-type": { + "import": "Importar tipo de widget", + "export": "Exportar tipo de widget", + "export-failed-error": "Imposible exportar tipo de widget: {{error}}", + "create-new-widget-type": "Crear nuevo tipo de widget", + "widget-type-file": "Tipo de archivo del widget", + "invalid-widget-type-file-error": "Imposible de importar tipo de widget: Estructura de datos inválida." + }, + "language": { + "language": "Lenguaje", + "en_US": "Inglés", + "ko_KR": "Coreano", + "zh_CN": "Chino", + "ru_RU": "Ruso", + "es_ES": "Español" + } + }; + angular.extend(locales, {'es_ES': es_ES}); +} \ No newline at end of file diff --git a/ui/src/app/locale/locale.constant-ko.js b/ui/src/app/locale/locale.constant-ko.js index bdc938863d..8b2d4bacb7 100644 --- a/ui/src/app/locale/locale.constant-ko.js +++ b/ui/src/app/locale/locale.constant-ko.js @@ -775,7 +775,10 @@ export default function addLocaleKorean(locales) { "language": { "language": "언어", "en_US": "영어", - "ko_KR": "한글" + "ko_KR": "한글", + "zh_CN": "중국어", + "ru_RU": "러시아어", + "es_ES": "스페인어" } }; angular.extend(locales, {'ko_KR': ko_KR}); diff --git a/ui/src/app/locale/locale.constant-ru.js b/ui/src/app/locale/locale.constant-ru.js new file mode 100644 index 0000000000..47c9460bbd --- /dev/null +++ b/ui/src/app/locale/locale.constant-ru.js @@ -0,0 +1,819 @@ +/* + * 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 function addLocaleRussian(locales) { + var ru_RU = { + "access": { + "unauthorized": "Неавторизированный", + "unauthorized-access": "Несанкционированный доступ", + "unauthorized-access-text": "Вы должны войти в систему для получения доступа к этому ресурсу!", + "access-forbidden": "Доступ запрещен", + "access-forbidden-text": "У вас нет прав доступа к этому ресурсу!
Для получения доступа попробуйте войти под другим пользователем.", + "refresh-token-expired": "Сессия истекла", + "refresh-token-failed": "Не удалось обновить сессию" + }, + "action": { + "activate": "Активировать", + "suspend": "Приостановить", + "save": "Сохранить", + "saveAs": "Сохранить как", + "cancel": "Отмена", + "ok": "ОК", + "delete": "Удалить", + "add": "Добавить", + "yes": "Да", + "no": "Нет", + "update": "Обновить", + "remove": "Удалить", + "search": "Поиск", + "assign": "Присвоить", + "unassign": "Отменить присвоение", + "share": "Поделиться", + "make-private": "Закрыть для общего доступа", + "apply": "Применить", + "apply-changes": "Применить изменения", + "edit-mode": "Режим редактирования", + "enter-edit-mode": "Режим редактирования", + "decline-changes": "Отменить изменения", + "close": "Закрыть", + "back": "Назад", + "run": "Запуск", + "sign-in": "Войти", + "edit": "Редактировать", + "view": "Просмотреть", + "create": "Создать", + "drag": "Переместить", + "refresh": "Обновить", + "undo": "Откатить", + "copy": "Копировать", + "paste": "Вставить", + "import": "Импортировать", + "export": "Экспортировать", + "share-via": "Поделиться в {{provider}}" + }, + "aggregation": { + "aggregation": "Агрегация", + "function": "Тип агрегации данных", + "limit": "Максимальное значение", + "group-interval": "Интервал группировки", + "min": "Мин", + "max": "Maкс", + "avg": "Среднее", + "sum": "Сумма", + "count": "Количество", + "none": "Без агрегации" + }, + "admin": { + "general": "Общие", + "general-settings": "Общие настройки", + "outgoing-mail": "Исходящая почта", + "outgoing-mail-settings": "Настройки исходящей почты", + "system-settings": "Системные настройки", + "test-mail-sent": "Пробное письмо успешно отправлено!", + "base-url": "Базовая URL", + "base-url-required": "Базовая URL обязательна.", + "mail-from": "Отправитель", + "mail-from-required": "Отправитель обязателен.", + "smtp-protocol": "SMTP протокол", + "smtp-host": "SMTP хост", + "smtp-host-required": "SMTP хост обязателен.", + "smtp-port": "SMTP порт", + "smtp-port-required": "SMTP порт обязателен.", + "smtp-port-invalid": "Недействительный SMTP порт.", + "timeout-msec": "Таймаут (мс)", + "timeout-required": "Таймаут обязателен.", + "timeout-invalid": "Недействительный таймаут.", + "enable-tls": "Включить TLS", + "send-test-mail": "Отправить пробное письмо" + }, + "attribute": { + "attributes": "Атрибуты", + "latest-telemetry": "Последняя телеметрия", + "attributes-scope": "Контекст атрибутов устройства", + "scope-latest-telemetry": "Последняя телеметрия", + "scope-client": "Клиентские атрибуты", + "scope-server": "Серверные атрибуты", + "scope-shared": "Общие атрибуты", + "add": "Добавить атрибут", + "key": "Ключ", + "key-required": "Ключ атрибута обязателен.", + "value": "Значение", + "value-required": "Значение атрибута обязательно.", + "delete-attributes-title": "Вы уверенны, что хотите удалить { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }? ", + "delete-attributes-text": "Внимание, после подтверждения выбранные атрибуты будут удалены.", + "delete-attributes": "Удалить атрибуты", + "enter-attribute-value": "Введите значение атрибута", + "show-on-widget": "Показать на виджете", + "widget-mode": "Виджет-режим", + "next-widget": "Следующий виджет", + "prev-widget": "Предыдущий виджет", + "add-to-dashboard": "Добавить на дашборд", + "add-widget-to-dashboard": "Добавить виджет на дашборд", + "selected-attributes": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }", + "selected-telemetry": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, 1 {1 параметр} few {# параметра} other {# параметров} } телеметрии" + }, + "confirm-on-exit": { + "message": "У вас есть несохраненные изменения. Вы точно хотите покинуть эту страницу?", + "html-message": "У вас есть несохраненные изменения.
Вы точно хотите покинуть эту страницу?", + "title": "Несохраненные изменения" + }, + "contact": { + "country": "Страна", + "city": "Город", + "state": "Штат", + "postal-code": "Почтовый код", + "postal-code-invalid": "Допустимы только цифры", + "address": "Адрес", + "address2": "Адрес 2", + "phone": "Телефон", + "email": "Эл. адрес", + "no-address": "Адрес не указан" + }, + "common": { + "username": "Имя пользователя", + "password": "Пароль", + "enter-username": "Введите имя пользователя", + "enter-password": "Введите пароль", + "enter-search": "Введите условие поиска" + }, + "customer": { + "customers": "Клиенты", + "management": "Управление клиентами", + "dashboard": "Дашборд клиентов", + "dashboards": "Дашборды клиентов", + "devices": "Устройства клиента", + "public-dashboards": "Общедоступные дашборды", + "public-devices": "Общедоступные устройства", + "add": "Добавить клиента", + "delete": "Удалить клиента", + "manage-customer-users": "Управление пользователями клиента", + "manage-customer-devices": "Управление устройствами клиента", + "manage-customer-dashboards": "Управление дашбордами клиента", + "manage-public-devices": "Управление общедоступными устройствами", + "manage-public-dashboards": "Управление общедоступными дашбордами", + "add-customer-text": "Добавить нового клиента", + "no-customers-text": "Клиенты не найдены", + "customer-details": "Подробности о клиенте", + "delete-customer-title": "Вы точно хотите удалить клиента '{{customerTitle}}'?", + "delete-customer-text": "Внимание, после подтверждения клиент и вся связанная с ним информация будут безвозвратно утеряны.", + "delete-customers-title": "Вы точно хотите удалить { count, plural, one {1 клиента} other {# клиентов} }?", + "delete-customers-action-title": "Удалить { count, plural, one {1 клиента} other {# клиентов} } }", + "delete-customers-text": "Внимание, после подтверждения клиенты и вся связанная с ними информация будут безвозвратно утеряны.", + "manage-users": "Управление пользователями", + "manage-devices": "Управление устройствами", + "manage-dashboards": "Управление дашбордами", + "title": "Имя", + "title-required": "Название обязательно.", + "description": "Описание" + }, + "datetime": { + "date-from": "Дата с", + "time-from": "Время с", + "date-to": "Дата до", + "time-to": "Время до" + }, + "dashboard": { + "dashboard": "Дашборд", + "dashboards": "Дашборды", + "management": "Управление дашбордами", + "view-dashboards": "Просмотреть дашборды", + "add": "Добавить дашборд", + "assign-dashboard-to-customer": "Прикрепить дашборд(ы) к клиенту", + "assign-dashboard-to-customer-text": "Пожалуйста, выберите дашборды, которые нужно прикрепить к клиенту", + "assign-to-customer-text": "Пожалуйста, выберите клиента, к которому нужно прикрепить дашборд(ы)", + "assign-to-customer": "Прикрепить к клиенту", + "unassign-from-customer": "Открепить от клиента", + "make-public": "Открыть дашборд для общего доступа", + "make-private": "Закрыть дашборд для общего доступа", + "no-dashboards-text": "Дашборды не найдены", + "no-widgets": "Виджеты не сконфигурированы", + "add-widget": "Добавить новый виджет", + "title": "Название", + "select-widget-title": "Выберите виджет", + "select-widget-subtitle": "Список доступных виджетов", + "delete": "Удалить дашборд", + "title-required": "Название обязательно.", + "description": "Описание", + "details": "Подробности", + "dashboard-details": "Подробности о дашборде", + "add-dashboard-text": "Добавить новый дашборд", + "assign-dashboards": "Прикрепить дашборды", + "assign-new-dashboard": "Прикрепить новый дашборд", + "assign-dashboards-text": "Прикрепить { count, plural, 1 {1 дашборд} other {# дашборда} } к клиенту", + "delete-dashboards": "Удалить дашборды", + "unassign-dashboards": "Открепить дашборды", + "unassign-dashboards-action-title": "Открепить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} } от клиента", + "delete-dashboard-title": "Вы точно хотите удалить дашборд '{{dashboardTitle}}'?", + "delete-dashboard-text": "Внимание, после подтверждения дашборд и все связанные с ним данные будут безвозвратно утеряны.", + "delete-dashboards-title": "Вы точно хотите удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?", + "delete-dashboards-action-title": "Удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }", + "delete-dashboards-text": "Внимание, после подтверждения дашборды и все связанные с ними данные будут безвозвратно утеряны.", + "unassign-dashboard-title": "Вы точно хотите открепить дашборд '{{dashboardTitle}}'?", + "unassign-dashboard-text": "После подтверждения дашборд не будет доступен клиенту.", + "unassign-dashboard": "Открепить дашборд", + "unassign-dashboards-title": "Вы точно хотите открепить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?", + "unassign-dashboards-text": "После подтверждения выбранные дашборды не будут доступны клиенту.", + "public-dashboard-title": "Теперь дашборд общедоступный", + "public-dashboard-text": "Теперь ваш дашборд {{dashboardTitle}} доступен всем по ссылке:", + "public-dashboard-notice": "Примечание: Для получения доступа к данным устройства нужно открыть общий доступ к этому устройству.", + "make-private-dashboard-title": "Вы точно хотите закрыть общий доступ к дашборду '{{dashboardTitle}}'?", + "make-private-dashboard-text": "После подтверждения дашборд будет закрыт для общего доступа.", + "make-private-dashboard": "Закрыть дашборд для общего доступа", + "socialshare-text": "'{{dashboardTitle}}' сделано ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' сделано ThingsBoard", + "select-dashboard": "Выберите дашборд", + "no-dashboards-matching": "Дашборд '{{dashboard}}' не найден.", + "dashboard-required": "Дашборд обязателен.", + "select-existing": "Выберите существующий дашборд", + "create-new": "Создать новый дашборд", + "new-dashboard-title": "Новое название дашборда", + "open-dashboard": "Открыть дашборд", + "set-background": "Установить фон", + "background-color": "Фоновый цвет", + "background-image": "Фоновая картинка", + "background-size-mode": "Размер фона", + "no-image": "Картинка не выбрана", + "drop-image": "Перетащите картинку или кликните для выбора файла.", + "settings": "Настройки", + "columns-count": "Количество колонок", + "columns-count-required": "Количество колонок обязательно.", + "min-columns-count-message": "Минимальное число колонок - 10.", + "max-columns-count-message": "Максимальное число колонок - 1000.", + "widgets-margins": "Величина отступа между виджетами", + "horizontal-margin": "Величина горизонтального отступа", + "horizontal-margin-required": "Величина горизонтального отступа обязательна.", + "min-horizontal-margin-message": "Минимальная величина горизонтального отступа - 0.", + "max-horizontal-margin-message": "Максимальная величина горизонтального отступа - 50.", + "vertical-margin": "Величина вертикального отступа", + "vertical-margin-required": "Величина вертикального отступа обязательна.", + "min-vertical-margin-message": "Минимальная величина вертикального отступа - 0.", + "max-vertical-margin-message": "Максимальная величина вертикального отступа - 50.", + "display-title": "Показать название дашборда", + "title-color": "Цвет названия", + "display-device-selection": "Показать выборку устройств", + "display-dashboard-timewindow": "Показать временное окно", + "display-dashboard-export": "Показать экспорт", + "import": "Импортировать дашборд", + "export": "Экспортировать дашборд", + "export-failed-error": "Не удалось экспортировать дашборд: {{error}}", + "create-new-dashboard": "Создать новый дашборд", + "dashboard-file": "Файл дашборда", + "invalid-dashboard-file-error": "Не удалось импортировать дашборд: неизвестная схема данных дашборда.", + "dashboard-import-missing-aliases-title": "Конфигурировать псевдонимы импортированного дашборда", + "create-new-widget": "Создать новый виджет", + "import-widget": "Импортировать новый виджет", + "widget-file": "Файл виджета", + "invalid-widget-file-error": "Не удалось импортировать виджет: неизвестная схема данных виджета.", + "widget-import-missing-aliases-title": "Конфигурировать псевдонимы импортированного виджета", + "open-toolbar": "Открыть панель инструментов", + "close-toolbar": "Закрыть панель инструментов", + "configuration-error": "Ошибка конфигурирования", + "alias-resolution-error-title": "Ошибка конфигурирования псевдонимов дашборда", + "invalid-aliases-config": "Не удалось найти устройства, соответствующие фильтру псевдонимов.
" + + "Пожалуйста, свяжитесь с администратором для устранения этой проблемы.", + "select-devices": "Выберите устройства", + "assignedToCustomer": "Прикреплен к клиенту", + "public": "Общедоступный", + "public-link": "Общедоступная ссылка", + "copy-public-link": "Скопировать общедоступную ссылку", + "public-link-copied-message": "Общедоступная ссылка на дашборд скопирована в буфер обмена" + }, + "datakey": { + "settings": "Настройки", + "advanced": "Дополнительно", + "label": "Метка", + "color": "Цвет", + "data-generation-func": "Функция генерации данных", + "use-data-post-processing-func": "Использовать функцию пост-обработки данных", + "configuration": "Конфигурация ключа данных", + "timeseries": "Выборка по времени", + "attributes": "Атрибуты", + "timeseries-required": "Выборка по времени обязательна.", + "timeseries-or-attributes-required": "Выборка по времени/атрибуты обязательны.", + "function-types": "Тип функции", + "function-types-required": "Тип функции обязателен." + }, + "datasource": { + "type": "Тип источника данных", + "add-datasource-prompt": "Пожалуйста, добавьте источник данных" + }, + "details": { + "edit-mode": "Режим редактирования", + "toggle-edit-mode": "Режим редактирования" + }, + "device": { + "device": "Устройство", + "device-required": "Устройство обязательно.", + "devices": "Устройства", + "management": "Управление устройствами", + "view-devices": "Просмотреть устройства", + "device-alias": "Псевдоним устройства", + "aliases": "Псевдонимы устройства", + "no-alias-matching": "'{{alias}}' не найден.", + "no-aliases-found": "Псевдонимы не найдены.", + "no-key-matching": "'{{key}}' не найден.", + "no-keys-found": "Ключи не найдены.", + "create-new-alias": "Создать новый!", + "create-new-key": "Создать новый!", + "duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.
В рамках дашборда псевдонимы устройств должны быть уникальными.", + "configure-alias": "Конфигурировать '{{alias}}' псевдоним", + "no-devices-matching": "Устройство '{{device}}' не найдено.", + "alias": "Псевдоним", + "alias-required": "Псевдоним устройства обязателен.", + "remove-alias": "Удалить псевдоним устройства", + "add-alias": "Добавить псевдоним устройства", + "name-starts-with": "Название начинается с", + "device-list": "Список устройств", + "use-device-name-filter": "Использовать фильтр", + "device-list-empty": "Устройства не выбраны.", + "device-name-filter-required": "Фильтр названия устройства обязателен.", + "device-name-filter-no-device-matched": "Устройства, названия которых начинаются с '{{device}}', не найдены.", + "add": "Добавить устройство", + "assign-to-customer": "Присвоить клиенту", + "assign-device-to-customer": "Присвоить устройство(а) клиенту", + "assign-device-to-customer-text": "Пожалуйста, выберите устройства, которые нужно присвоить клиенту", + "make-public": "Открыть общий доступ к устройству", + "make-private": "Закрыть общий доступ к устройству", + "no-devices-text": "Устройства не найдены", + "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить устройство(а)", + "device-details": "Подробности об устройстве", + "add-device-text": "Добавить новое устройство", + "credentials": "Учетные данные", + "manage-credentials": "Управление учетными данными", + "delete": "Удалить устройство", + "assign-devices": "Присвоить устройство", + "assign-devices-text": "Присвоить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } клиенту", + "delete-devices": "Удалить устройства", + "unassign-from-customer": "Отменить присвоение клиенту", + "unassign-devices": "Отменить присвоение устройств", + "unassign-devices-action-title": "Отменить присвоение { count, plural, one {1 устройства} few {# устройств} other {# устройств} } клиенту", + "assign-new-device": "Присвоить новое устройство", + "make-public-device-title": "Вы точно хотите открыть общий доступ к устройству '{{deviceName}}'?", + "make-public-device-text": "После подтверждения устройство и все связанные с ним данные будут общедоступными.", + "make-private-device-title": "Вы точно хотите закрыть общий доступ к устройству '{{deviceName}}'", + "make-private-device-text": "После подтверждения устройство и все связанные с ним данные будут закрыты для общего доступа.", + "view-credentials": "Просмотреть учетные данные", + "delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?", + "delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.", + "delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?", + "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }", + "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..", + "unassign-device-title": "Вы точно хотите отменить присвоение устройства '{{deviceName}}'?", + "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.", + "unassign-device": "Отменить присвоение устройства", + "unassign-devices-title": "Вы точно хотите отменить присвоение { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }?", + "unassign-devices-text": "После подтверждения выбранные устройства будут недоступны клиенту.", + "device-credentials": "Учетные данные устройства", + "credentials-type": "Тип учетных данных", + "access-token": "Токен", + "access-token-required": "Токен обязателен.", + "access-token-invalid": "Длина токена должна быть от 1 до 20 символов.", + "rsa-key": "Открытый ключ RSA", + "rsa-key-required": "Открытый ключ RSA обязателен.", + "secret": "Секрет", + "secret-required": "Секрет обязателен.", + "name": "Название", + "name-required": "Название обязательно.", + "description": "Описание", + "events": "События", + "details": "Подробности", + "copyId": "Копировать идентификатор устройства", + "copyAccessToken": "Копировать токен", + "idCopiedMessage": "Идентификатор устройства скопирован в буфер обмена", + "accessTokenCopiedMessage": "Токен устройства скопирован в буфер обмена", + "assignedToCustomer": "Присвоен клиенту", + "unable-delete-device-alias-title": "Не удалось удалить псевдоним устройства", + "unable-delete-device-alias-text": "Не удалось удалить псевдоним '{{deviceAlias}}' устройства, т.к. он используется следующими виджетами:
{{widgetsList}}", + "is-gateway": "Гейтвей", + "public": "Общедоступный", + "device-public": "Устройство общедоступно" + }, + "dialog": { + "close": "Закрыть диалог" + }, + "error": { + "unable-to-connect": "Не удалось подключиться к серверу! Пожалуйста, проверьте интернет-соединение.", + "unhandled-error-code": "Код необработанной ошибки: {{errorCode}}", + "unknown-error": "Неизвестная ошибка" + }, + "event": { + "event-type": "Тип события", + "type-alarm": "Аварийное оповещение", + "type-error": "Ошибка", + "type-lc-event": "Событие жизненного цикла", + "type-stats": "Статистика", + "no-events-prompt": "События не найдены", + "error": "Ошибка", + "alarm": "Аварийное оповещение", + "event-time": "Время возникновения события", + "server": "Сервер", + "body": "Тело", + "method": "Метод", + "event": "Событие", + "status": "Статус", + "success": "Успех", + "failed": "Неудача", + "messages-processed": "Сообщения обработаны", + "errors-occurred": "Возникли ошибки" + }, + "fullscreen": { + "expand": "Во весь экран", + "exit": "Выйти из полноэкранного режима", + "toggle": "Во весь экран", + "fullscreen": "Полноэкранный режим" + }, + "function": { + "function": "Функция" + }, + "grid": { + "delete-item-title": "Вы точно хотите удалить этот объект?", + "delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.", + "delete-items-title": "Вы точно хотите удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }?", + "delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов}", + "delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.", + "add-item-text": "Добавить новый объект", + "no-items-text": "Объекты не найдены", + "item-details": "Подробности об объекте", + "delete-item": "Удалить объект", + "delete-items": "Удалить объекты", + "scroll-to-top": "Прокрутка к началу" + }, + "help": { + "goto-help-page": "Перейти к справке" + }, + "home": { + "home": "Главная", + "profile": "Профиль", + "logout": "Выйти из системы", + "menu": "Меню", + "avatar": "Аватар", + "open-user-menu": "Открыть меню пользователя" + }, + "import": { + "no-file": "Файл не выбран", + "drop-file": "Перетащите JSON файл или кликните для выбора файла." + }, + "item": { + "selected": "Выбранные" + }, + "js-func": { + "no-return-error": "Функция должна возвращать значение!", + "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!" + }, + "legend": { + "position": "Расположение легенды", + "show-max": "Показать максимальное значение", + "show-min": "Показать минимальное значение", + "show-avg": "Показать среднее значение", + "show-total": "Показать сумму", + "settings": "Настройки легенды", + "min": "Мин", + "max": "Макс", + "avg": "Среднее", + "total": "Сумма" + }, + "login": { + "login": "Войти", + "request-password-reset": "Запрос на сброс пароля", + "reset-password": "Сбросить пароль", + "create-password": "Создать пароль", + "passwords-mismatch-error": "Введенные пароли должны быть одинаковыми!", + "password-again": "Введите пароль еще раз", + "sign-in": "Пожалуйста, войдите в систему", + "username": "Имя пользователя (эл. адрес)", + "remember-me": "Запомнить меня", + "forgot-password": "Забыли пароль?", + "password-reset": "Пароль сброшен", + "new-password": "Новый пароль", + "new-password-again": "Повторите новый пароль", + "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", + "email": "Эл. адрес" + }, + "plugin": { + "plugins": "Плагины", + "delete": "Удалить плагин", + "activate": "Активировать плагин", + "suspend": "Приостановить плагин", + "active": "Активный", + "suspended": "Приостановлен", + "name": "Название", + "name-required": "Название обязательно.", + "description": "Описание", + "add": "Добавить плагин", + "delete-plugin-title": "Вы точно хотите удалить плагин '{{pluginName}}'?", + "delete-plugin-text": "Внимание, после подтверждения плагин и все связанные с ним данные будут безвозвратно утеряны.", + "delete-plugins-title": "Вы точно хотите удалить { count, plural, one {1 плагин} few {# плагина} other {# плагинов} }?", + "delete-plugins-action-title": "Удалить { count, plural, one {1 плагин} few {# плагина} other {# плагинов} } }", + "delete-plugins-text": "Внимание, после подтверждения выбранные плагины и все связанные с ними данные будут безвозвратно утеряны.", + "add-plugin-text": "Добавить новый плагин", + "no-plugins-text": "Плагины не найдены", + "plugin-details": "Подробности о плагине", + "api-token": "API токен", + "api-token-required": "API токен обязателен.", + "type": "Тип плагина", + "type-required": "Тип плагина обязателен.", + "configuration": "Настройки плагина", + "system": "Системный", + "select-plugin": "Выберите плагин", + "plugin": "Плагин", + "no-plugins-matching": "Плагин '{{plugin}}' не найден.", + "plugin-required": "Плагин обязателен.", + "plugin-require-match": "Пожалуйста, выберите существующий плагин.", + "events": "События", + "details": "Подробности", + "import": "Импортировать плагин", + "export": "Экспортировать плагин", + "export-failed-error": "Не удалось экспортировать плагин: {{error}}", + "create-new-plugin": "Создать новый плагин", + "plugin-file": "Файл плагина", + "invalid-plugin-file-error": "Не удалось импортировать плагин: неизвестная схема данных плагина." + }, + "position": { + "top": "Верх", + "bottom": "Низ", + "left": "Левый край", + "right": "Правый край" + }, + "profile": { + "profile": "Профиль", + "change-password": "Изменить пароль", + "current-password": "Текущий пароль" + }, + "rule": { + "rules": "Правила", + "delete": "Удалить правило", + "activate": "Активировать правило", + "suspend": "Приостановить правило", + "active": "Активное", + "suspended": "Приостановлены", + "name": "Название", + "name-required": "Название обязательно.", + "description": "Описание", + "add": "Добавить правило", + "delete-rule-title": "Вы точно хотите удалить правило '{{ruleName}}'?", + "delete-rule-text": "Внимание, после подтверждения правило и все связанные с ним данные будут безвозвратно утеряны.", + "delete-rules-title": "Вы точно хотите удалить { count, plural, one {1 правило} few {# правила} other {# правил} }?", + "delete-rules-action-title": "Удалить { count, plural, one {1 правило} few {# правила} other {# правил} }", + "delete-rules-text": "Внимание, после подтверждения выбранные правила и все связанные с ними данные будут безвозвратно утеряны.", + "add-rule-text": "Добавить новое правило", + "no-rules-text": "Правила не найдены", + "rule-details": "Подробности о правиле", + "filters": "Фильтры", + "filter": "Фильтр", + "add-filter-prompt": "Пожалуйста, добавьте фильтр", + "remove-filter": "Удалить фильтр", + "add-filter": "Добавить фильтр", + "filter-name": "Название фильтра", + "filter-type": "Тип фильтра", + "edit-filter": "Редактировать фильтр", + "view-filter": "Просмотреть фильтр", + "component-name": "Название", + "component-name-required": "Название обязательно.", + "component-type": "Тип", + "component-type-required": "Тип обязателен.", + "processor": "Обработчик", + "no-processor-configured": "Обработчики не сконфигурированы", + "create-processor": "Создать обработчик", + "processor-name": "Название обработчика", + "processor-type": "Тип обработчика", + "plugin-action": "Действие плагина", + "action-name": "Название действия", + "action-type": "Тип действия", + "create-action-prompt": "Пожалуйста, создайте действие", + "create-action": "Создать действие", + "details": "Подробности", + "events": "События", + "system": "Системное", + "import": "Импортировать правило", + "export": "Экспортировать правило", + "export-failed-error": "Не удалось экспортировать правило: {{error}}", + "create-new-rule": "Создать новое правило", + "rule-file": "Файл правила", + "invalid-rule-file-error": "Не удалось импортировать правило: неизвестная схема данных правила." + }, + "rule-plugin": { + "management": "Управление плагинами и правилами" + }, + "tenant": { + "tenants": "Владельцы", + "management": "Управление владельцами", + "add": "Добавить владельца", + "admins": "Администраторы", + "manage-tenant-admins": "Управление администраторами владельца", + "delete": "Удалить владельца", + "add-tenant-text": "Добавить нового владельца", + "no-tenants-text": "Владельцы не найдены", + "tenant-details": "Подробности об владельце", + "delete-tenant-title": "Вы точно хотите удалить владельца '{{tenantTitle}}'?", + "delete-tenant-text": "Внимание, после подтверждения владелец и все связанные с ним данные будут безвозвратно утеряны.", + "delete-tenants-title": "Вы точно хотите удалить { count, plural, one {1 владельца} other {# владельцев} }?", + "delete-tenants-action-title": "Удалить { count, plural, one {1 владельца} other {# владельцев} }", + "delete-tenants-text": "Внимание, после подтверждения выбранные Владельцы и все связанные с ними данные будут безвозвратно утеряны.", + "title": "Имя", + "title-required": "Имя обязательно.", + "description": "Описание" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }", + "minutes-interval": "{ minutes, plural, one {1 минута} few {# минуты} other {# минут} }", + "hours-interval": "{ hours, plural, one {1 час} few {# часа} other {# часов} }", + "days-interval": "{ days, plural, one {1 день} few {# дня} other {# дней} }", + "days": "Дни", + "hours": "Часы", + "minutes": "Минуты", + "seconds": "Секунды", + "advanced": "Дополнительно" + }, + "timewindow": { + "days": "{ days, plural, one {1 день} few {# дня} other {# дней} }", + "hours": "{ hours, plural, one {1 час} few {# часа} other {# часов} }", + "minutes": "{ minutes, plural, one {1 минута} few {# минуты} other {# минут} }", + "seconds": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }", + "realtime": "Режим реального времени", + "history": "История", + "last-prefix": "Последние", + "period": "с {{ startTime }} до {{ endTime }}", + "edit": "Изменить временное окно", + "date-range": "Диапазон дат", + "last": "Последние", + "time-period": "Период времени" + }, + "user": { + "users": "Пользователи", + "customer-users": "Пользователи клиента", + "tenant-admins": "Администраторы владельца", + "sys-admin": "Системный администратор", + "tenant-admin": "Администратор владельца", + "customer": "Клиент", + "anonymous": "Аноним", + "add": "Добавить пользователя", + "delete": "Удалить пользователя", + "add-user-text": "Добавить нового пользователя", + "no-users-text": "Пользователи не найдены", + "user-details": "Подробности о пользователе", + "delete-user-title": "Вы точно хотите удалить пользователя '{{userEmail}}'?", + "delete-user-text": "Внимание, после подтверждения пользователь и все связанные с ним данные будут безвозвратно утеряны.", + "delete-users-title": "Вы точно хотите удалить { count, plural, one {1 пользователя} other {# пользователей} }?", + "delete-users-action-title": "Удалить { count, plural, one {1 пользователя} other {# пользователей} }", + "delete-users-text": "Внимание, после подтверждения выбранные пользователи и все связанные с ними данные будут безвозвратно утеряны.", + "activation-email-sent-message": "Активационное письмо успешно отправлено!", + "resend-activation": "Повторить отправку активационного письма", + "email": "Эл. адрес", + "email-required": "Эл. адрес обязателен.", + "first-name": "Имя", + "last-name": "Фамилия", + "description": "Описание", + "default-dashboard": "Дашборд по умолчанию", + "always-fullscreen": "Всегда во весь экран" + }, + "value": { + "type": "Тип значения", + "string": "Строка", + "string-value": "Строковое значение", + "integer": "Целое число", + "integer-value": "Целочисленное значение", + "invalid-integer-value": "Неправильный формат целого числа", + "double": "Число двойной точности", + "double-value": "Значение двойной точности", + "boolean": "Логический тип", + "boolean-value": "Логическое значение", + "false": "Ложь", + "true": "Правда" + }, + "widget": { + "widget-library": "Галерея виджетов", + "widget-bundle": "Набор виджетов", + "select-widgets-bundle": "Выберите набор виджетов", + "management": "Управление виджетами", + "editor": "Редактор виджетов", + "widget-type-not-found": "Ошибка при загрузке конфигурации виджета.
Возможно, связанный с ней\n тип виджета уже удален.", + "widget-type-load-error": "Не удалось загрузить виджет по следующим причинам:", + "remove": "Удалить виджет", + "edit": "Редактировать виджет", + "remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?", + "remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.", + "timeseries": "Выборка по времени", + "latest-values": "Последние значения", + "rpc": "Управляющий виджет", + "static": "Статический виджет", + "select-widget-type": "Выберите тип виджета", + "missing-widget-title-error": "Укажите название виджета!", + "widget-saved": "Виджет сохранен", + "unable-to-save-widget-error": "Не удалось сохранить виджет! Виджет содержит ошибки!", + "save": "Сохранить виджет", + "saveAs": "Сохранить виджет как", + "save-widget-type-as": "Сохранить тип виджета как", + "save-widget-type-as-text": "Пожалуйста, введите название виджета и/или укажите целевой набор виджетов", + "toggle-fullscreen": "Во весь экран", + "run": "Запустить виджет", + "title": "Название виджета", + "title-required": "Название виджета обязательно.", + "type": "Тип виджета", + "resources": "Ресурсы", + "resource-url": "JavaScript/CSS URL", + "remove-resource": "Удалить ресурс", + "add-resource": "Добавить ресурс", + "html": "HTML", + "tidy": "Форматировать", + "css": "CSS", + "settings-schema": "Схема конфигурации", + "datakey-settings-schema": "Схема конфигурации ключа данных", + "javascript": "Javascript", + "remove-widget-type-title": "Вы точно хотите удалить виджет '{{widgetName}}'?", + "remove-widget-type-text": "Внимание, после подтверждения тип виджета и все связанные с ним данные будут безвозвратно утеряны.", + "remove-widget-type": "Удалить тип виджета", + "add-widget-type": "Добавить новый тип виджета", + "widget-type-load-failed-error": "Не удалось загрузить тип виджета!", + "widget-template-load-failed-error": "Не удалось загрузить шаблон виджета!", + "add": "Добавить виджет", + "undo": "Откатить изменения в виджете", + "export": "Экспортировать виджет" + }, + "widgets-bundle": { + "current": "Текущий набор", + "widgets-bundles": "Наборы виджетов", + "add": "Добавить набор виджетов", + "delete": "Удалить набор виджетов", + "title": "Название", + "title-required": "Название обязательно.", + "add-widgets-bundle-text": "Добавить новый набор виджетов", + "no-widgets-bundles-text": "Наборы виджетов не найдены", + "empty": "Пустой набор виджетов", + "details": "Подробности", + "widgets-bundle-details": "Подробности о наборе виджетов", + "delete-widgets-bundle-title": "Вы точно хотите удалить набор виджетов '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Внимание, после подтверждения набор виджетов и все связанные с ним данные будут безвозвратно утеряны.", + "delete-widgets-bundles-title": "Вы точно хотите удалить { count, plural, one {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }?", + "delete-widgets-bundles-action-title": "Удалить { count, plural, one {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }", + "delete-widgets-bundles-text": "Внимание, после подтверждения выбранные наборы виджетов и все связанные с ними данные будут безвозвратно утеряны..", + "no-widgets-bundles-matching": "Набор виджетов '{{widgetsBundle}}' не найден.", + "widgets-bundle-required": "Набор виджетов обязателен.", + "system": "Системный", + "import": "Импортировать набор виджетов", + "export": "Экспортировать набор виджетов", + "export-failed-error": "Не удалось экспортировать набор виджетов: {{error}}", + "create-new-widgets-bundle": "Создать новый набор виджетов", + "widgets-bundle-file": "Файл набора виджетов", + "invalid-widgets-bundle-file-error": "Не удалось импортировать набор виджетов: неизвестная схема данных набора виджетов." + }, + "widget-config": { + "data": "Данные", + "settings": "Настройки", + "advanced": "Дополнительно", + "title": "Название", + "general-settings": "Общие настройки", + "display-title": "Показать название", + "drop-shadow": "Тень", + "enable-fullscreen": "Во весь экран", + "background-color": "Цвет фона", + "text-color": "Цвет текста", + "padding": "Отступ", + "title-style": "Стиль названия", + "mobile-mode-settings": "Настройки мобильного режима", + "order": "Порядок", + "height": "Высота", + "units": "Специальный символ после значения", + "decimals": "Количество цифр после запятой", + "timewindow": "Временное окно", + "use-dashboard-timewindow": "Использовать временное окно дашборда", + "display-legend": "Показать легенду", + "datasources": "Источники данных", + "datasource-type": "Тип", + "datasource-parameters": "Параметры", + "remove-datasource": "Удалить источник данных", + "add-datasource": "Добавить источник данных", + "target-device": "Целевое устройство" + }, + "widget-type": { + "import": "Импортировать тип виджета", + "export": "Экспортировать тип виджета", + "export-failed-error": "Не удалось экспортировать тип виджета: {{error}}", + "create-new-widget-type": "Создать новый тип виджета", + "widget-type-file": "Файл типа виджета", + "invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета." + }, + "language": { + "language": "Язык", + "en_US": "Английский", + "ko_KR": "Корейский", + "zh_CN": "Китайский", + "ru_RU": "Русский", + "es_ES": "испанский" + + } + }; + angular.extend(locales, {'ru_RU': ru_RU}); +} \ No newline at end of file diff --git a/ui/src/app/locale/locale.constant-zh.js b/ui/src/app/locale/locale.constant-zh.js new file mode 100644 index 0000000000..56e4d57905 --- /dev/null +++ b/ui/src/app/locale/locale.constant-zh.js @@ -0,0 +1,820 @@ +/* + * 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 function addLocaleChinese(locales) { + var zh_CN = { + "access" : { + "unauthorized" : "未授权", + "unauthorized-access" : "未授权访问", + "unauthorized-access-text" : "您需要登陆才能访问这个资源!", + "access-forbidden" : "禁止访问", + "access-forbidden-text" : "您没有访问此位置的权限
如果您仍希望访问此位置,请尝试使用其他用户登录。", + "refresh-token-expired" : "会话已过期", + "refresh-token-failed" : "无法刷新会话" + }, + "action" : { + "activate" : "激活", + "suspend" : "暂停", + "save" : "保存", + "saveAs" : "另存为", + "cancel" : "取消", + "ok" : "确定", + "delete" : "删除", + "add" : "添加", + "yes" : "是", + "no" : "否", + "update" : "更新", + "remove" : "移除", + "search" : "查询", + "assign" : "分配", + "unassign" : "取消分配", + "share" : "分享", + "make-private" : "私有", + "apply" : "应用", + "apply-changes" : "应用更改", + "edit-mode" : "编辑模式", + "enter-edit-mode" : "进入编辑模式", + "decline-changes" : "拒绝变更", + "close" : "关闭", + "back" : "后退", + "run" : "运行", + "sign-in" : "登录!", + "edit" : "编辑", + "view" : "查看", + "create" : "创建", + "drag" : "拖拽", + "refresh" : "刷新", + "undo" : "撤销", + "copy" : "复制", + "paste" : "粘贴", + "import" : "导入", + "export" : "导出", + "share-via" : "通过 {{provider}}分享" + }, + "aggregation" : { + "aggregation" : "聚合", + "function" : "数据聚合功能", + "limit" : "最大值", + "group-interval" : "分组间隔", + "min" : "最少值", + "max" : "最大值", + "avg" : "平均值", + "sum" : "求和", + "count" : "计数", + "none" : "空" + }, + "admin" : { + "general" : "常规", + "general-settings" : "常规设置", + "outgoing-mail" : "发送邮件", + "outgoing-mail-settings" : "发送邮件设置", + "system-settings" : "系统设置", + "test-mail-sent" : "测试邮件发送成功!", + "base-url" : "基本URL", + "base-url-required" : "基本URL是必须的。", + "mail-from" : "邮件来自", + "mail-from-required" : "邮件发件人是必须的。", + "smtp-protocol" : "SMTP协议", + "smtp-host" : "SMTP主机", + "smtp-host-required" : "SMTP主机是必须的。", + "smtp-port" : "SMTP端口", + "smtp-port-required" : "您必须提供一个smtp端口。", + "smtp-port-invalid" : "这看起来不是有效的smtp端口。", + "timeout-msec" : "超时(ms)", + "timeout-required" : "超时是必须的。", + "timeout-invalid" : "这看起来不像有效的超时值。", + "enable-tls" : "启用TLS", + "send-test-mail" : "发送测试邮件" + }, + "attribute" : { + "attributes" : "属性", + "latest-telemetry" : "最新遥测", + "attributes-scope" : "设备属性范围", + "scope-latest-telemetry" : "最新遥测", + "scope-client" : "客户端属性", + "scope-server" : "服务端属性", + "scope-shared" : "共享属性", + "add" : "添加属性", + "key" : "键", + "key-required" : "属性键是必需的。", + "value" : "值", + "value-required" : "属性值是必需的。", + "delete-attributes-title" : "您确定要删除 { count, select, 1 {1 attribute} other {# attributes} }吗?", + "delete-attributes-text" : "注意,确认后所有选中的属性都会被删除。", + "delete-attributes" : "删除属性", + "enter-attribute-value" : "输入属性值", + "show-on-widget" : "在小部件上显示", + "widget-mode" : "小部件模式", + "next-widget" : "下一个小部件", + "prev-widget" : "上一个小部件", + "add-to-dashboard" : "添加到仪表板", + "add-widget-to-dashboard" : "将小部件添加到仪表板", + "selected-attributes" : "{ count, select, 1 {1 attribute} other {# attributes} } 被选中", + "selected-telemetry" : "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } 被选中" + }, + "confirm-on-exit" : { + "message" : "您有未保存的更改。确定要离开此页吗?", + "html-message" : "您有未保存的更改。
确定要离开此页面吗?", + "title" : "未保存的更改" + }, + "contact" : { + "country" : "国家", + "city" : "城市", + "state" : "州", + "postal-code" : "邮政编码", + "postal-code-invalid" : "只允许数字。", + "address" : "地址", + "address2" : "地址2", + "phone" : "手机", + "email" : "邮箱", + "no-address" : "无地址" + }, + "common" : { + "username" : "用户名", + "password" : "密码", + "enter-username" : "输入用户名", + "enter-password" : "输入密码", + "enter-search" : "输入检索条件" + }, + "customer" : { + "customers" : "客户", + "management" : "客户管理", + "dashboard" : "客户仪表板", + "dashboards" : "客户仪表板", + "devices" : "客户设备", + "public-dashboards" : "公共仪表板", + "public-devices" : "公共设备", + "add" : "添加客户", + "delete" : "删除客户", + "manage-customer-users" : "管理客户用户", + "manage-customer-devices" : "管理客户设备", + "manage-customer-dashboards" : "管理客户仪表板", + "manage-public-devices" : "管理公共设备", + "manage-public-dashboards" : "管理公共仪表板", + "add-customer-text" : "添加新客户", + "no-customers-text" : "没有找到客户", + "customer-details" : "客户详情", + "delete-customer-title" : "您确定要删除客户'{{customerTitle}}'吗?", + "delete-customer-text" : "小心!确认后,客户及其所有相关数据将不可恢复。", + "delete-customers-title" : "您确定要删除 { count, select, 1 {1 customer} other {# customers} }?吗", + "delete-customers-action-title" : "删除 { count, select, 1 {1 customer} other {# customers} }", + "delete-customers-text" : "小心!确认后,所有选定的客户将被删除,所有相关数据将不可恢复。", + "manage-users" : "管理用户", + "manage-devices" : "管理设备", + "manage-dashboards" : "管理仪表板", + "title" : "标题", + "title-required" : "需要标题。", + "description" : "描述" + }, + "datetime" : { + "date-from" : "日期从", + "time-from" : "时间从", + "date-to" : "日期到", + "time-to" : "时间到" + }, + "dashboard" : { + "dashboard" : "仪表板", + "dashboards" : "仪表板库", + "management" : "仪表板管理", + "view-dashboards" : "查看仪表板", + "add" : "添加仪表板", + "assign-dashboard-to-customer" : "将仪表板分配给客户", + "assign-dashboard-to-customer-text" : "请选择要分配给客户的仪表板", + "assign-to-customer-text" : "请选择客户分配仪表板", + "assign-to-customer" : "分配给客户", + "unassign-from-customer" : "取消分配客户", + "make-public" : "使仪表板公有", + "make-private" : "使仪表板私有", + "no-dashboards-text" : "没有找到仪表板", + "no-widgets" : "没有配置小部件", + "add-widget" : "添加新的小部件", + "title" : "标题", + "select-widget-title" : "选择小部件", + "select-widget-subtitle" : "可用的小部件类型列表", + "delete" : "删除仪表板", + "title-required" : "需要标题。", + "description" : "描述", + "details" : "详情", + "dashboard-details" : "仪表板详情", + "add-dashboard-text" : "添加新的仪表板", + "assign-dashboards" : "分配仪表板", + "assign-new-dashboard" : "分配新的仪表板", + "assign-dashboards-text" : "分配 { count, select, 1 {1 dashboard} other {# dashboards} } 给客户", + "delete-dashboards" : "删除仪表板", + "unassign-dashboards" : "取消分配仪表板", + "unassign-dashboards-action-title" : "取消分配 { count, select, 1 {1 dashboard} other {# dashboards} } from customer", + "delete-dashboard-title" : "您确定要删除仪表板 '{{dashboardTitle}}'吗?", + "delete-dashboard-text" : "小心!确认后仪表板及其所有相关数据将不可恢复。", + "delete-dashboards-title" : "你确定你要删除 { count, select, 1 {1 dashboard} other {# dashboards} }吗?", + "delete-dashboards-action-title" : "删除 { count, select, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text" : "小心!确认后所有选定的仪表板将被删除,所有相关数据将不可恢复。", + "unassign-dashboard-title" : "您确定要取消分配仪表板 '{{dashboardTitle}}'吗?", + "unassign-dashboard-text" : "确认后,面板将被取消分配,客户将无法访问。", + "unassign-dashboard" : "取消分配仪表板", + "unassign-dashboards-title" : "您确定要取消分配仪表板 { count, select, 1 {1 dashboard} other {# dashboards} } 吗?", + "unassign-dashboards-text" : "确认后,所有选定的仪表板将被取消分配,客户将无法访问。", + "public-dashboard-title" : "仪表板现已公布", + "public-dashboard-text" : "你的仪表板 {{dashboardTitle}} 已被公开,可通过如下 链接访问:", + "public-dashboard-notice" : "提示: 不要忘记将相关设备公开以访问其数据。", + "make-private-dashboard-title" : "您确定要使仪表板 '{{dashboardTitle}}' 私有吗?", + "make-private-dashboard-text" : "确认后,仪表板将被私有,不能被其他人访问。", + "make-private-dashboard" : "仪表板私有", + "socialshare-text" : "'{{dashboardTitle}}' 由ThingsBoard提供支持", + "socialshare-title" : "'{{dashboardTitle}}' 由ThingsBoard提供支持", + "select-dashboard" : "选择仪表板", + "no-dashboards-matching" : "找不到符合 '{{dashboard}}' 的仪表板。", + "dashboard-required" : "仪表板是必需的。", + "select-existing" : "选择现有仪表板", + "create-new" : "创建新的仪表板", + "new-dashboard-title" : "新仪表板标题", + "open-dashboard" : "打开仪表板", + "set-background" : "设置背景", + "background-color" : "背景颜色", + "background-image" : "背景图片", + "background-size-mode" : "背景大小模式", + "no-image" : "无图像选择", + "drop-image" : "拖拽图像或单击以选择要上传的文件。", + "settings" : "设置", + "columns-count" : "列数", + "columns-count-required" : "需要列数。", + "min-columns-count-message" : "只允许最少10列", + "max-columns-count-message" : "只允许最多1000列", + "widgets-margins" : "部件间边距", + "horizontal-margin" : "水平边距", + "horizontal-margin-required" : "需要水平边距值。", + "min-horizontal-margin-message" : "只允许0作为最小水平边距值。", + "max-horizontal-margin-message" : "只允许50作为最大水平边距值。", + "vertical-margin" : "垂直边距", + "vertical-margin-required" : "需要垂直边距值。", + "min-vertical-margin-message" : "只允许0作为最小垂直边距值。", + "max-vertical-margin-message" : "只允许50作为最大垂直边距值。", + "display-title" : "显示仪表板标题", + "title-color" : "标题颜色", + "display-device-selection" : "显示设备选择", + "display-dashboard-timewindow" : "显示时间窗口", + "display-dashboard-export" : "显示导出", + "import" : "导入仪表板", + "export" : "导出仪表板", + "export-failed-error" : "无法导出仪表板: {{error}}", + "create-new-dashboard" : "创建新的仪表板", + "dashboard-file" : "仪表板文件", + "invalid-dashboard-file-error" : "无法导入仪表板: 仪表板数据结构无效。", + "dashboard-import-missing-aliases-title" : "配置导入仪表板使用的别名", + "create-new-widget" : "创建新小部件", + "import-widget" : "导入小部件", + "widget-file" : "小部件文件", + "invalid-widget-file-error" : "无法导入窗口小部件: 窗口小部件数据结构无效。", + "widget-import-missing-aliases-title" : "配置导入的窗口小部件使用的别名", + "open-toolbar" : "打开仪表板工具栏", + "close-toolbar" : "关闭工具栏", + "configuration-error" : "配置错误", + "alias-resolution-error-title" : "仪表板别名配置错误", + "invalid-aliases-config" : "无法找到与某些别名过滤器匹配的任何设备。
" + + "请联系您的管理员以解决此问题。", + "select-devices" : "选择设备", + "assignedToCustomer" : "分配给客户", + "public" : "公共", + "public-link" : "公共链接", + "copy-public-link" : "复制公共链接", + "public-link-copied-message" : "仪表板的公共链接已被复制到剪贴板" + }, + "datakey" : { + "settings": "设置", + "advanced": "高级", + "label": "标签", + "color": "颜色", + "data-generation-func": "数据生成功能", + "use-data-post-processing-func": "使用数据后处理功能", + "configuration": "数据键配置", + "timeseries": "时间序列", + "attributes": "属性", + "timeseries-required": "需要设备时间序列。", + "timeseries-or-attributes-required": "设备时间/属性是必需的。", + "function-types": "函数类型", + "function-types-required": "需要函数类型。" + }, + "datasource" : { + "type": "数据源类型", + "add-datasource-prompt": "请添加数据源" + }, + "details" : { + "edit-mode": "编辑模式", + "toggle-edit-mode": "切换编辑模式" + }, + "device" : { + "device": "设备", + "device-required": "设备是必需的", + "devices": "设备", + "management": "设备管理", + "view-devices": "查看设备", + "device-alias": "设备别名", + "aliases": "设备别名", + "no-alias-matching" : "'{{alias}}' 没有找到。", + "no-aliases-found" : "找不到别名。", + "no-key-matching" : "'{{key}}' 没有找到。", + "no-keys-found" : "找不到密钥。", + "create-new-alias": "创建一个新的!", + "create-new-key": "创建一个新的!", + "duplicate-alias-error" : "找到重复别名 '{{alias}}'。
设备别名必须是唯一的。", + "configure-alias" : "配置 '{{alias}}' 别名", + "no-devices-matching" : "找不到与 '{{device}}' 匹配的设备。", + "alias" : "别名", + "alias-required" : "需要设备别名。", + "remove-alias": "删除设备别名", + "add-alias": "添加设备别名", + "name-starts-with" : "名称前缀", + "device-list" : "设备列表", + "use-device-name-filter" : "使用过滤器", + "device-list-empty" : "没有被选中的设备", + "device-name-filter-required" : "设备名称过滤器是必需得。", + "device-name-filter-no-device-matched" : "找不到以'{{device}}' 开头的设备。", + "add" : "添加设备", + "assign-to-customer": "分配给客户", + "assign-device-to-customer": "将设备分配给客户", + "assign-device-to-customer-text": "请选择要分配给客户的设备", + "make-public" : "公有", + "make-private" : "私有", + "no-devices-text": "找不到设备", + "assign-to-customer-text": "请选择客户分配设备", + "device-details": "设备详细信息", + "add-device-text": "添加新设备", + "credentials": "凭据", + "manage-credentials": "管理凭据", + "delete": "删除设备", + "assign-devices": "分配设备", + "assign-devices-text": "将{count,select,1 {1 device} other {# devices}}分配给客户", + "delete-devices": "删除设备", + "unassign-from-customer": "取消分配客户", + "unassign-devices": "取消分配设备", + "unassign-devices-action-title": "从客户处取消分配{count,select,1 {1 device} other {# devices}}", + "assign-new-device": "分配新设备", + "make-public-device-title" : "您确定要将设备 '{{deviceName}}' 设为公开吗?", + "make-public-device-text" : "确认后,设备及其所有数据将被公开并可被其他人访问。", + "make-private-device-title" : "您确定要将设备 '{{deviceName}}' 设为私有吗?", + "make-private-device-text" : "确认后,设备及其所有数据将被私有化,不被其他人访问。", + "view-credentials": "查看凭据", + "delete-device-title": "您确定要删除设备的{{deviceName}}吗?", + "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。", + "delete-devices-title": "您确定要删除{count,select,1 {1 device} other {# devices}} 吗?", + "delete-devices-action-title": "删除 {count,select,1 {1 device} other {# devices}}", + "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。", + "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?", + "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", + "unassign-device": "取消分配设备", + "unassign-devices-title": "您确定要取消分配{count,select,1 {1 device} other {# devices}} 吗?", + "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", + "device-credentials": "设备凭据", + "credentials-type": "凭据类型", + "access-token": "访问令牌", + "access-token-required": "需要访问令牌", + "access-token-invalid": "访问令牌长度必须为1到20个字符。", + "rsa-key": "RSA公钥", + "rsa-key-required": "需要RSA公钥", + "secret": "密钥", + "secret-required": "密钥是必需的", + "name": "名称", + "name-required": "名称是必需的。", + "description": "说明", + "events": "事件", + "details": "详细信息", + "copyId": "复制设备ID", + "copyAccessToken": "复制访问令牌", + "idCopiedMessage": "设备ID已复制到剪贴板", + "accessTokenCopiedMessage": "设备访问令牌已复制到剪贴板", + "assignedToCustomer": "分配给客户", + "unable-delete-device-alias-title": "无法删除设备别名", + "unable-delete-device-alias-text": "设备别名 '{{deviceAlias}}' 不能够被删除,因为它被下列部件所使用:
{{widgetsList}}", + "is-gateway": "是网关", + "public" : "公共", + "device-public" : "设备是公共的" + }, + "dialog" : { + "close" : "关闭对话框" + }, + "error" : { + "unable-to-connect": "无法连接到服务器!请检查您的互联网连接。", + "unhandled-error-code": "未处理的错误代码: {{errorCode}}", + "unknown-error": "未知错误" + }, + "event" : { + "event-type": "事件类型", + "type-alarm": "报警", + "type-error": "错误", + "type-lc-event": "生命周期事件", + "type-stats": "类型统计", + "no-events-prompt": "找不到事件", + "error": "错误", + "alarm": "报警", + "event-time": "事件时间", + "server": "服务器", + "body": "整体", + "method": "方法", + "event": "事件", + "status": "状态", + "success": "成功", + "failed": "失败", + "messages-processed": "消息处理", + "errors-occurred": "错误发生" + }, + "fullscreen" : { + "expand": "展开到全屏", + "exit": "退出全屏", + "toggle": "切换全屏模式", + "fullscreen": "全屏" + }, + "function" : { + "function" : "函数" + }, + "grid" : { + "delete-item-title": "您确定要删除此项吗?", + "delete-item-text": "注意,确认后此项及其所有相关数据将变得不可恢复。", + "delete-items-title" : "你确定你要删除 { count, select, 1 {1 item} other {# items} }吗?", + "delete-items-action-title" : "删除 { count, select, 1 {1 item} other {# items} }", + "delete-items-text": "注意,确认后所有选择的项目将被删除,所有相关数据将不可恢复。", + "add-item-text": "添加新项目", + "no-items-text": "没有找到项目", + "item-details": "项目详细信息", + "delete-item": "删除项目", + "delete-items": "删除项目", + "scroll-to-top": "滚动到顶部" + }, + "help" : { + "goto-help-page" : "转到帮助页面" + }, + "home" : { + "home": "首页", + "profile": "属性", + "logout": "注销", + "menu": "菜单", + "avatar": "头像", + "open-user-menu": "打开用户菜单" + }, + "import" : { + "no-file" : "没有选择文件", + "drop-file" : "拖动一个JSON文件或者单击以选择要上传的文件。" + }, + "item" : { + "selected" : "选择" + }, + "js-func" : { + "no-return-error": "函数必须返回值!", + "return-type-mismatch": "函数必须返回 '{{type}}' 类型的值!" + }, + "legend" : { + "position" : "图例位置", + "show-max" : "显示最大值", + "show-min" : "显示最小值", + "show-avg" : "显示平均值", + "show-total" : "显示总数", + "settings" : "图例设置", + "min" : "最小值", + "max" : "最大值", + "avg" : "平均值", + "total" : "总数" + }, + "login" : { + "login": "登录", + "request-password-reset": "请求密码重置", + "reset-password": "重置密码", + "create-password": "创建密码", + "passwords-mismatch-error": "输入的密码必须相同!", + "password-again": "再次输入密码", + "sign-in": "登录 ", + "username": "用户名(电子邮件)", + "remember-me": "记住我", + "forgot-password": "忘记密码?", + "password-reset": "密码重置", + "new-password": "新密码", + "new-password-again": "再次输入新密码", + "password-link-sent-message": "密码重置链接已成功发送!", + "email": "电子邮件" + }, + "plugin" : { + "plugins" : "插件", + "delete" : "删除插件", + "activate" : "激活插件", + "suspend" : "暂停插件", + "active" : "激活", + "suspended" : "暂停", + "name" : "名称", + "name-required" : "名称是必填项。", + "description" : "描述", + "add" : "添加插件", + "delete-plugin-title" : "你确定要删除插件 '{{pluginName}}' 吗?", + "delete-plugin-text" : "小心!确认后,插件和所有相关数据将不可恢复。", + "delete-plugins-title" : "你确定你要删除 { count, select, 1 {1 plugin} other {# plugins} } 吗?", + "delete-plugins-action-title" : "删除 { count, select, 1 {1 plugin} other {# plugins} }", + "delete-plugins-text" : "小心!确认后,所有选定的插件将被删除,所有相关数据将不可恢复。", + "add-plugin-text" : "添加新的插件", + "no-plugins-text" : "没有找到插件", + "plugin-details" : "插件详细信息", + "api-token" : "API令牌", + "api-token-required" : "API令牌是必需的。", + "type" : "插件类型", + "type-required" : "插件类型是必需的。", + "configuration" : "插件配置", + "system" : "系统", + "select-plugin" : "选择插件", + "plugin" : "插件", + "no-plugins-matching" : "没有找到匹配'{{plugin}}'的插件。", + "plugin-required" : "插件是必需的。", + "plugin-require-match" : "请选择一个现有的插件。", + "events" : "事件", + "details" : "详情", + "import" : "导入插件", + "export" : "导出插件", + "export-failed-error" : "无法导出插件:{{error}}", + "create-new-plugin" : "创建新的插件", + "plugin-file" : "插件文件", + "invalid-plugin-file-error" : "无法导入插件:插件数据结构无效。" + }, + "position" : { + "top" : "顶部", + "bottom" : "底部", + "left" : "左侧", + "right" : "右侧" + }, + "profile" : { + "profile": "属性", + "change-password": "更改密码", + "current-password": "当前密码" + }, + "rule" : { + "rules" : "规则", + "delete" : "删除规则", + "activate" : "激活规则", + "suspend" : "暂停规则", + "active" : "激活", + "suspended" : "暂停", + "name" : "名称", + "name-required" : "名称是必填项。", + "description" : "描述", + "add" : "添加规则", + "delete-rule-title" : "您确定要删除规则'{{ruleName}}'吗?", + "delete-rule-text" : "小心!确认后,规则和所有相关数据将不可恢复。", + "delete-rules-title" : "你确定要删除 {count, select, 1 {1 rule} other {# rules}} 吗?", + "delete-rules-action-title" : "删除 { count, select, 1 {1 rule} other {# rules} }", + "delete-rules-text" : "小心!确认后,所有选定的规则将被删除,所有相关数据将不可恢复。", + "add-rule-text" : "添加新规则", + "no-rules-text" : "没有找到规则", + "rule-details" : "规则详情", + "filters" : "过滤器", + "filter" : "过滤器", + "add-filter-prompt" : "请添加过滤器", + "remove-filter" : "删除过滤器", + "add-filter" : "添加过滤器", + "filter-name" : "过滤器名称", + "filter-type" : "过滤器类型", + "edit-filter" : "编辑过滤器", + "view-filter" : "查看过滤器", + "component-name" : "名称", + "component-name-required" : "名称是必填项。", + "component-type" : "类型", + "component-type-required" : "类型是必填项。", + "processor" : "处理器", + "no-processor-configured" : "未配置处理器", + "create-processor" : "创建处理器", + "processor-name" : "处理器名称", + "processor-type" : "处理器类型", + "plugin-action" : "插件动作", + "action-name" : "动作名称", + "action-type" : "动作类型", + "create-action-prompt" : "请创建动作", + "create-action" : "创建动作", + "details" : "详情", + "events" : "事件", + "system" : "系统", + "import" : "导入规则", + "export" : "导出规则", + "export-failed-error" : "无法导出规则:{{error}}", + "create-new-rule" : "创建新规则", + "rule-file" : "规则文件", + "invalid-rule-file-error" : "无法导入规则:规则数据结构无效。" + }, + "rule-plugin" : { + "management" : "规则和插件管理" + }, + "tenant" : { + "tenants" : "租户", + "management" : "租户管理", + "add" : "添加租户", + "admins" : "管理员", + "manage-tenant-admins" : "管理租户管理员", + "delete" : "删除租户", + "add-tenant-text" : "添加新租户", + "no-tenants-text" : "没有找到租户", + "tenant-details" : "租客详情", + "delete-tenant-title" : "您确定要删除租户'{{tenantTitle}}'吗?", + "delete-tenant-text" : "小心!确认后,租户和所有相关数据将不可恢复。", + "delete-tenants-title" : "您确定要删除 {count,select,1 {1 tenant} other {# tenants}} 吗?", + "delete-tenants-action-title" : "删除 { count, select, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text" : "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", + "title" : "标题", + "title-required" : "标题是必填项。", + "description" : "描述" + }, + "timeinterval" : { + "seconds-interval" : "{ seconds, select, 1 {1 second} other {# seconds} }", + "minutes-interval" : "{ minutes, select, 1 {1 minute} other {# minutes} }", + "hours-interval" : "{ hours, select, 1 {1 hour} other {# hours} }", + "days-interval" : "{ days, select, 1 {1 day} other {# days} }", + "days" : "天", + "hours" : "时", + "minutes" : "分", + "seconds" : "秒", + "advanced" : "高级" + }, + "timewindow" : { + "days" : "{ days, select, 1 { day } other {# days } }", + "hours" : "{ hours, select, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes" : "{ minutes, select, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds" : "{ seconds, select, 0 { second } 1 {1 second } other {# seconds } }", + "realtime" : "实时", + "history" : "历史", + "last-prefix" : "最后", + "period" : "从 {{ startTime }} 到 {{ endTime }}", + "edit" : "编辑时间窗口", + "date-range" : "日期范围", + "last" : "最后", + "time-period" : "时间段" + }, + "user" : { + "users" : "用户", + "customer-users" : "客户用户", + "tenant-admins" : "租户管理员", + "sys-admin" : "系统管理员", + "tenant-admin" : "租户管理员", + "customer" : "客户", + "anonymous" : "匿名", + "add" : "添加用户", + "delete" : "删除用户", + "add-user-text" : "添加新用户", + "no-users-text" : "找不到用户", + "user-details" : "用户详细信息", + "delete-user-title" : "您确定要删除用户 '{{userEmail}}' 吗?", + "delete-user-text" : "小心!确认后,用户和所有相关数据将不可恢复。", + "delete-users-title" : "你确定你要删除 { count, select, 1 {1 user} other {# users} } 吗?", + "delete-users-action-title" : "删除 { count, select, 1 {1 user} other {# users} }", + "delete-users-text" : "小心!确认后,所有选定的用户将被删除,所有相关数据将不可恢复。", + "activation-email-sent-message" : "激活电子邮件已成功发送!", + "resend-activation" : "重新发送激活", + "email" : "电子邮件", + "email-required" : "电子邮件是必需的。", + "first-name" : "名字", + "last-name" : "姓", + "description" : "描述", + "default-dashboard" : "默认面板", + "always-fullscreen" : "始终全屏" + }, + "value" : { + "type" : "值类型", + "string" : "字符串", + "string-value" : "字符串值", + "integer" : "数字", + "integer-value" : "数字值", + "invalid-integer-value" : "整数值无效", + "double" : "双精度小数", + "double-value" : "双精度小数值", + "boolean" : "布尔", + "boolean-value" : "布尔值", + "false" : "假", + "true" : "真" + }, + "widget" : { + "widget-library" : "小部件库", + "widget-bundle" : "小部件包", + "select-widgets-bundle" : "选择小部件包", + "management" : "小部件管理", + "editor" : "小部件编辑器", + "widget-type-not-found" : "加载小部件配置时出现问题。
可能关联的\n 小部件类型已删除。", + "widget-type-load-error" : "由于以下错误,小工具未加载:", + "remove" : "删除小部件", + "edit" : "编辑小部件", + "remove-widget-title" : "您确定要删除小部件 '{{widgetTitle}}' 吗?", + "remove-widget-text" : "确认后,窗口小部件和所有相关数据将不可恢复。", + "timeseries" : "时间序列", + "latest-values" : "最新值", + "rpc" : "控件小部件", + "static" : "静态小部件", + "select-widget-type" : "选择窗口小部件类型", + "missing-widget-title-error" : "小部件标题必须指定!", + "widget-saved" : "小部件已保存", + "unable-to-save-widget-error" : "无法保存窗口小部件! 小部件有错误!", + "save" : "保存小部件", + "saveAs" : "将小部件另存为", + "save-widget-type-as" : "将小部件类型另存为", + "save-widget-type-as-text" : "请输入新的小部件标题和/或选择目标小部件包", + "toggle-fullscreen" : "切换全屏", + "run" : "运行小部件", + "title" : "小部件标题", + "title-required" : "需要小部件标题。", + "type" : "小部件类型", + "resources" : "资源", + "resource-url" : "JavaScript/CSS URL", + "remove-resource" : "删除资源", + "add-resource" : "添加资源", + "html" : "HTML", + "tidy" : "整理", + "css" : "CSS", + "settings-schema" : "设置模式", + "datakey-settings-schema" : "数据键设置模式", + "javascript" : "Javascript", + "remove-widget-type-title" : "您确定要删除小部件类型 '{{widgetName}}'吗?", + "remove-widget-type-text" : "确认后,窗口小部件类型和所有相关数据将不可恢复。", + "remove-widget-type" : "删除小部件类型", + "add-widget-type" : "添加新的小部件类型", + "widget-type-load-failed-error" : "无法加载小部件类型!", + "widget-template-load-failed-error" : "无法加载小部件模板!", + "add" : "添加小部件", + "undo" : "撤消小部件更改", + "export" : "导出小部件" + }, + "widgets-bundle" : { + "current" : "当前包", + "widgets-bundles" : "小部件包", + "add" : "添加小部件包", + "delete" : "删除小部件包", + "title" : "标题", + "title-required" : "标题是必填项。", + "add-widgets-bundle-text" : "添加新的小部件包", + "no-widgets-bundles-text" : "找不到小部件包", + "empty" : "小部件包是空的", + "details" : "详情", + "widgets-bundle-details" : "小部件包详细信息", + "delete-widgets-bundle-title" : "您确定要删除小部件包 '{{widgetsBundleTitle}}'吗?", + "delete-widgets-bundle-text" : "小心!确认后,小部件包和所有相关数据将不可恢复。", + "delete-widgets-bundles-title" : "你确定你要删除 { count, select, 1 {1 widgets bundle} other {# widgets bundles} } 吗?", + "delete-widgets-bundles-action-title" : "删除 { count, select, 1 {1 widgets bundle} other {# widgets bundles} }", + "delete-widgets-bundles-text" : "小心!确认后,所有选定的小部件包将被删除,所有相关数据将不可恢复。", + "no-widgets-bundles-matching" : "没有找到与 '{{widgetsBundle}}' 匹配的小部件包。", + "widgets-bundle-required" : "需要小部件包。", + "system" : "系统", + "import" : "导入小部件包", + "export" : "导出小部件包", + "export-failed-error" : "无法导出小部件包: {{error}}", + "create-new-widgets-bundle" : "创建新的小部件包", + "widgets-bundle-file" : "小部件包文件", + "invalid-widgets-bundle-file-error" : "无法导入小部件包:无效的小部件包数据结构。" + }, + "widget-config" : { + "data" : "数据", + "settings" : "设置", + "advanced" : "高级", + "title" : "标题", + "general-settings" : "常规设置", + "display-title" : "显示标题", + "drop-shadow" : "阴影", + "enable-fullscreen" : "启用全屏", + "background-color" : "背景颜色", + "text-color" : "文字颜色", + "padding" : "填充", + "title-style" : "标题风格", + "mobile-mode-settings" : "移动模式设置", + "order" : "顺序", + "height" : "高度", + "units" : "特殊符号展示值", + "decimals" : "浮点数后的位数", + "timewindow" : "时间窗口", + "use-dashboard-timewindow" : "使用仪表板的时间窗口", + "display-legend" : "显示图例", + "datasources" : "数据源", + "datasource-type" : "类型", + "datasource-parameters" : "参数", + "remove-datasource" : "移除数据源", + "add-datasource" : "添加数据源", + "target-device" : "目标设备" + }, + "widget-type" : { + "import" : "导入小部件类型", + "export" : "导出小部件类型", + "export-failed-error" : "无法导出小部件类型: {{error}}", + "create-new-widget-type" : "创建新的小部件类型", + "widget-type-file" : "小部件类型文件", + "invalid-widget-type-file-error" : "无法导入小部件类型:无效的小部件类型数据结构。" + }, + "language" : { + "language" : "语言", + "en_US" : "英语", + "ko_KR" : "韩语", + "zh_CN" : "汉语", + "ru_RU" : "俄语", + "es_ES": "西班牙語" + } + }; + angular.extend(locales, { + 'zh_CN' : zh_CN + }); +} \ No newline at end of file diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 3de229e9aa..1fc45f7681 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -43,6 +43,8 @@ export default angular.module('thingsboard.locale', []) "search": "Search", "assign": "Assign", "unassign": "Unassign", + "share": "Share", + "make-private": "Make private", "apply": "Apply", "apply-changes": "Apply changes", "edit-mode": "Edit mode", @@ -61,7 +63,8 @@ export default angular.module('thingsboard.locale', []) "copy": "Copy", "paste": "Paste", "import": "Import", - "export": "Export" + "export": "Export", + "share-via": "Share via {{provider}}" }, "aggregation": { "aggregation": "Aggregation", @@ -98,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", @@ -154,11 +207,19 @@ 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", "manage-customer-devices": "Manage customer devices", "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", @@ -168,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", @@ -191,6 +257,8 @@ export default angular.module('thingsboard.locale', []) "assign-to-customer-text": "Please select the customer to assign the dashboard(s)", "assign-to-customer": "Assign to customer", "unassign-from-customer": "Unassign from customer", + "make-public": "Make dashboard public", + "make-private": "Make dashboard private", "no-dashboards-text": "No dashboards found", "no-widgets": "No widgets configured", "add-widget": "Add new widget", @@ -219,6 +287,14 @@ export default angular.module('thingsboard.locale', []) "unassign-dashboard": "Unassign dashboard", "unassign-dashboards-title": "Are you sure you want to unassign { count, select, 1 {1 dashboard} other {# dashboards} }?", "unassign-dashboards-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the customer.", + "public-dashboard-title": "Dashboard is now public", + "public-dashboard-text": "Your dashboard {{dashboardTitle}} is now public and accessible via next public link:", + "public-dashboard-notice": "Note: Do not forget to make related devices public in order to access their data.", + "make-private-dashboard-title": "Are you sure you want to make the dashboard '{{dashboardTitle}}' private?", + "make-private-dashboard-text": "After the confirmation the dashboard will be made private and won't be accessible by others.", + "make-private-dashboard": "Make dashboard private", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", "select-dashboard": "Select dashboard", "no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.", "dashboard-required": "Dashboard is required.", @@ -248,6 +324,9 @@ 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-entities-selection": "Display entities selection", + "display-dashboard-timewindow": "Display timewindow", + "display-dashboard-export": "Display export", "import": "Import dashboard", "export": "Export dashboard", "export-failed-error": "Unable to export dashboard: {{error}}", @@ -267,7 +346,11 @@ export default angular.module('thingsboard.locale', []) "invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.
" + "Please contact your administrator in order to resolve this issue.", "select-devices": "Select devices", - "assignedToCustomer": "Assigned to customer" + "assignedToCustomer": "Assigned to customer", + "public": "Public", + "public-link": "Public link", + "copy-public-link": "Copy public link", + "public-link-copied-message": "Dashboard public link has been copied to clipboard" }, "datakey": { "settings": "Settings", @@ -279,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", @@ -323,6 +406,8 @@ export default angular.module('thingsboard.locale', []) "assign-to-customer": "Assign to customer", "assign-device-to-customer": "Assign Device(s) To Customer", "assign-device-to-customer-text": "Please select the devices to assign to the customer", + "make-public": "Make device public", + "make-private": "Make device private", "no-devices-text": "No devices found", "assign-to-customer-text": "Please select the customer to assign the device(s)", "device-details": "Device details", @@ -337,6 +422,10 @@ export default angular.module('thingsboard.locale', []) "unassign-devices": "Unassign devices", "unassign-devices-action-title": "Unassign { count, select, 1 {1 device} other {# devices} } from customer", "assign-new-device": "Assign new device", + "make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?", + "make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.", + "make-private-device-title": "Are you sure you want to make the device '{{deviceName}}' private?", + "make-private-device-text": "After the confirmation the device and all its data will be made private and won't be accessible by others.", "view-credentials": "View credentials", "delete-device-title": "Are you sure you want to delete the device '{{deviceName}}'?", "delete-device-text": "Be careful, after the confirmation the device and all related data will become unrecoverable.", @@ -369,7 +458,9 @@ export default angular.module('thingsboard.locale', []) "assignedToCustomer": "Assigned to customer", "unable-delete-device-alias-title": "Unable to delete device alias", "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", - "is-gateway": "Is gateway" + "is-gateway": "Is gateway", + "public": "Public", + "device-public": "Device is public" }, "dialog": { "close": "Close dialog" @@ -379,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", @@ -509,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", @@ -572,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" @@ -594,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} }", @@ -778,8 +912,11 @@ export default angular.module('thingsboard.locale', []) "language": { "language": "Language", "en_US": "English", - "ko_KR": "Korean" + "ko_KR": "Korean", + "zh_CN": "Chinese", + "ru_RU": "Russian", + "es_ES": "Spanish" } } } - ).name; \ No newline at end of file + ).name; 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. --> +
- + + + + + + + + + + + + + + + + +
diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 4cc2192bea..340b7eb948 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -34,39 +34,12 @@ export default class TbFlot { this.chartType = chartType || 'line'; var settings = ctx.settings; - var colors = []; - for (var i = 0; i < ctx.data.length; i++) { - var series = ctx.data[i]; - colors.push(series.dataKey.color); - var keySettings = series.dataKey.settings; - - series.lines = { - fill: keySettings.fillLines === true, - show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true - }; - - series.points = { - show: false, - radius: 8 - }; - if (keySettings.showPoints === true) { - series.points.show = true; - series.points.lineWidth = 5; - series.points.radius = 3; - } - if (this.chartType === 'line' && settings.smoothLines && !series.points.show) { series.curvedLines = { apply: true } } - var lineColor = tinycolor(series.dataKey.color); - lineColor.setAlpha(.75); - - series.highlightColor = lineColor.toRgbString(); - - } ctx.tooltip = $('#flot-series-tooltip'); if (ctx.tooltip.length === 0) { ctx.tooltip = $("
"); @@ -183,7 +156,6 @@ export default class TbFlot { }; var options = { - colors: colors, title: null, subtitle: null, shadowSize: settings.shadowSize || 4, @@ -290,14 +262,10 @@ export default class TbFlot { } options.series.bars ={ show: true, - barWidth: ctx.timeWindow.interval * 0.6, lineWidth: 0, fill: 0.9 } } - - options.xaxis.min = ctx.timeWindow.minTime; - options.xaxis.max = ctx.timeWindow.maxTime; } else if (this.chartType === 'pie') { options.series = { pie: { @@ -340,55 +308,121 @@ export default class TbFlot { this.options = options; + if (this.ctx.defaultSubscription) { + this.init(this.ctx.$container, this.ctx.defaultSubscription); + } + } + + init($element, subscription) { + this.subscription = subscription; + this.$element = $element; + var colors = []; + for (var i = 0; i < this.subscription.data.length; i++) { + var series = this.subscription.data[i]; + colors.push(series.dataKey.color); + var keySettings = series.dataKey.settings; + + series.lines = { + fill: keySettings.fillLines === true, + show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true + }; + + series.points = { + show: false, + radius: 8 + }; + if (keySettings.showPoints === true) { + series.points.show = true; + series.points.lineWidth = 5; + series.points.radius = 3; + } + + if (this.chartType === 'line' && this.ctx.settings.smoothLines && !series.points.show) { + series.curvedLines = { + apply: true + } + } + + var lineColor = tinycolor(series.dataKey.color); + lineColor.setAlpha(.75); + + series.highlightColor = lineColor.toRgbString(); + + } + this.options.colors = colors; + if (this.chartType === 'line' || this.chartType === 'bar') { + if (this.chartType === 'bar') { + this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + } + this.options.xaxis.min = this.subscription.timeWindow.minTime; + this.options.xaxis.max = this.subscription.timeWindow.maxTime; + } + this.checkMouseEvents(); + if (this.ctx.plot) { + this.ctx.plot.destroy(); + } if (this.chartType === 'pie' && this.ctx.animatedPie) { this.ctx.pieDataAnimationDuration = 250; - this.ctx.pieData = angular.copy(this.ctx.data); + this.pieData = angular.copy(this.subscription.data); this.ctx.pieRenderedData = []; this.ctx.pieTargetData = []; - for (i = 0; i < this.ctx.data.length; i++) { - this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0]) - ? this.ctx.data[i].data[0][1] : 0; + for (i = 0; i < this.subscription.data.length; i++) { + this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0]) + ? this.subscription.data[i].data[0][1] : 0; } this.pieDataRendered(); - this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options); + this.ctx.plot = $.plot(this.$element, this.pieData, this.options); } else { - this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); + this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options); } } update() { - if (!this.isMouseInteraction && this.ctx.plot) { - if (this.chartType === 'line' || this.chartType === 'bar') { - this.options.xaxis.min = this.ctx.timeWindow.minTime; - this.options.xaxis.max = this.ctx.timeWindow.maxTime; - this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime; - this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime; - if (this.chartType === 'bar') { - this.options.series.bars.barWidth = this.ctx.timeWindow.interval * 0.6; - this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6; - } - this.ctx.plot.setData(this.ctx.data); - this.ctx.plot.setupGrid(); - this.ctx.plot.draw(); - } else if (this.chartType === 'pie') { - if (this.ctx.animatedPie) { - this.nextPieDataAnimation(true); - } else { - this.ctx.plot.setData(this.ctx.data); + if (this.updateTimeoutHandle) { + this.ctx.$scope.$timeout.cancel(this.updateTimeoutHandle); + this.updateTimeoutHandle = null; + } + if (this.subscription) { + if (!this.isMouseInteraction && this.ctx.plot) { + if (this.chartType === 'line' || this.chartType === 'bar') { + this.options.xaxis.min = this.subscription.timeWindow.minTime; + this.options.xaxis.max = this.subscription.timeWindow.maxTime; + this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime; + this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime; + if (this.chartType === 'bar') { + this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + this.ctx.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + } + this.ctx.plot.setData(this.subscription.data); + this.ctx.plot.setupGrid(); this.ctx.plot.draw(); + } else if (this.chartType === 'pie') { + if (this.ctx.animatedPie) { + this.nextPieDataAnimation(true); + } else { + this.ctx.plot.setData(this.subscription.data); + this.ctx.plot.draw(); + } } + } else if (this.isMouseInteraction && this.ctx.plot){ + var tbFlot = this; + this.updateTimeoutHandle = this.ctx.$scope.$timeout(function() { + tbFlot.update(); + }, 30, false); } } } resize() { - this.ctx.plot.resize(); - if (this.chartType !== 'pie') { - this.ctx.plot.setupGrid(); + if (this.ctx.plot) { + this.ctx.plot.resize(); + if (this.chartType !== 'pie') { + this.ctx.plot.setupGrid(); + } + this.ctx.plot.draw(); } - this.ctx.plot.draw(); } static get pieSettingsSchema() { @@ -708,17 +742,19 @@ export default class TbFlot { var enabled = !this.ctx.isMobile && !this.ctx.isEdit; if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) { this.mouseEventsEnabled = enabled; - if (enabled) { - this.enableMouseEvents(); - } else { - this.disableMouseEvents(); - } - if (this.ctx.plot) { - this.ctx.plot.destroy(); - if (this.chartType === 'pie' && this.ctx.animatedPie) { - this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options); + if (this.$element) { + if (enabled) { + this.enableMouseEvents(); } else { - this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); + this.disableMouseEvents(); + } + if (this.ctx.plot) { + this.ctx.plot.destroy(); + if (this.chartType === 'pie' && this.ctx.animatedPie) { + this.ctx.plot = $.plot(this.$element, this.pieData, this.options); + } else { + this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options); + } } } } @@ -732,8 +768,8 @@ export default class TbFlot { } enableMouseEvents() { - this.ctx.$container.css('pointer-events',''); - this.ctx.$container.addClass('mouse-events'); + this.$element.css('pointer-events',''); + this.$element.addClass('mouse-events'); this.options.selection = { mode : 'x' }; var tbFlot = this; @@ -794,33 +830,33 @@ export default class TbFlot { tbFlot.ctx.plot.unhighlight(); } }; - this.ctx.$container.bind('plothover', this.flotHoverHandler); + this.$element.bind('plothover', this.flotHoverHandler); } if (!this.flotSelectHandler) { this.flotSelectHandler = function (event, ranges) { tbFlot.ctx.plot.clearSelection(); - tbFlot.ctx.timewindowFunctions.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to); + tbFlot.subscription.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to); }; - this.ctx.$container.bind('plotselected', this.flotSelectHandler); + this.$element.bind('plotselected', this.flotSelectHandler); } if (!this.dblclickHandler) { this.dblclickHandler = function () { - tbFlot.ctx.timewindowFunctions.onResetTimewindow(); + tbFlot.subscription.onResetTimewindow(); }; - this.ctx.$container.bind('dblclick', this.dblclickHandler); + this.$element.bind('dblclick', this.dblclickHandler); } if (!this.mousedownHandler) { this.mousedownHandler = function () { tbFlot.isMouseInteraction = true; }; - this.ctx.$container.bind('mousedown', this.mousedownHandler); + this.$element.bind('mousedown', this.mousedownHandler); } if (!this.mouseupHandler) { this.mouseupHandler = function () { tbFlot.isMouseInteraction = false; }; - this.ctx.$container.bind('mouseup', this.mouseupHandler); + this.$element.bind('mouseup', this.mouseupHandler); } if (!this.mouseleaveHandler) { this.mouseleaveHandler = function () { @@ -829,38 +865,38 @@ export default class TbFlot { tbFlot.ctx.plot.unhighlight(); tbFlot.isMouseInteraction = false; }; - this.ctx.$container.bind('mouseleave', this.mouseleaveHandler); + this.$element.bind('mouseleave', this.mouseleaveHandler); } } disableMouseEvents() { - this.ctx.$container.css('pointer-events','none'); - this.ctx.$container.removeClass('mouse-events'); + this.$element.css('pointer-events','none'); + this.$element.removeClass('mouse-events'); this.options.selection = { mode : null }; if (this.flotHoverHandler) { - this.ctx.$container.unbind('plothover', this.flotHoverHandler); + this.$element.unbind('plothover', this.flotHoverHandler); this.flotHoverHandler = null; } if (this.flotSelectHandler) { - this.ctx.$container.unbind('plotselected', this.flotSelectHandler); + this.$element.unbind('plotselected', this.flotSelectHandler); this.flotSelectHandler = null; } if (this.dblclickHandler) { - this.ctx.$container.unbind('dblclick', this.dblclickHandler); + this.$element.unbind('dblclick', this.dblclickHandler); this.dblclickHandler = null; } if (this.mousedownHandler) { - this.ctx.$container.unbind('mousedown', this.mousedownHandler); + this.$element.unbind('mousedown', this.mousedownHandler); this.mousedownHandler = null; } if (this.mouseupHandler) { - this.ctx.$container.unbind('mouseup', this.mouseupHandler); + this.$element.unbind('mouseup', this.mouseupHandler); this.mouseupHandler = null; } if (this.mouseleaveHandler) { - this.ctx.$container.unbind('mouseleave', this.mouseleaveHandler); + this.$element.unbind('mouseleave', this.mouseleaveHandler); this.mouseleaveHandler = null; } } @@ -952,10 +988,10 @@ export default class TbFlot { for (var i = 0; i < this.ctx.pieTargetData.length; i++) { var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0; this.ctx.pieRenderedData[i] = value; - if (!this.ctx.pieData[i].data[0]) { - this.ctx.pieData[i].data[0] = [0,0]; + if (!this.pieData[i].data[0]) { + this.pieData[i].data[0] = [0,0]; } - this.ctx.pieData[i].data[0][1] = value; + this.pieData[i].data[0][1] = value; } } @@ -963,9 +999,9 @@ export default class TbFlot { if (start) { this.finishPieDataAnimation(); this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now(); - for (var i = 0; i < this.ctx.data.length; i++) { - this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0]) - ? this.ctx.data[i].data[0][1] : 0; + for (var i = 0; i < this.subscription.data.length; i++) { + this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0]) + ? this.subscription.data[i].data[0][1] : 0; } } if (this.ctx.pieAnimationCaf) { @@ -992,12 +1028,12 @@ export default class TbFlot { var prevValue = this.ctx.pieRenderedData[i]; var targetValue = this.ctx.pieTargetData[i]; var value = prevValue + (targetValue - prevValue) * progress; - if (!this.ctx.pieData[i].data[0]) { - this.ctx.pieData[i].data[0] = [0,0]; + if (!this.pieData[i].data[0]) { + this.pieData[i].data[0] = [0,0]; } - this.ctx.pieData[i].data[0][1] = value; + this.pieData[i].data[0][1] = value; } - this.ctx.plot.setData(this.ctx.pieData); + this.ctx.plot.setData(this.pieData); this.ctx.plot.draw(); this.ctx.pieAnimationLastTime = time; } @@ -1007,7 +1043,7 @@ export default class TbFlot { finishPieDataAnimation() { this.pieDataRendered(); - this.ctx.plot.setData(this.ctx.pieData); + this.ctx.plot.setData(this.pieData); this.ctx.plot.draw(); } } diff --git a/ui/src/app/widget/lib/google-map.js b/ui/src/app/widget/lib/google-map.js index 9722839541..0e104ebfa9 100644 --- a/ui/src/app/widget/lib/google-map.js +++ b/ui/src/app/widget/lib/google-map.js @@ -182,7 +182,7 @@ export default class TbGoogleMap { /* eslint-enable no-undef */ /* eslint-disable no-undef */ - createMarker(location, settings) { + createMarker(location, settings, onClickListener) { var height = 34; var pinColor = settings.color.substr(1); var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + pinColor, @@ -217,10 +217,21 @@ export default class TbGoogleMap { this.updateMarkerImage(marker, settings, settings.markerImage, settings.markerImageSize || 34); } - this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); + if (settings.displayTooltip) { + this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); + } + + if (onClickListener) { + marker.addListener('click', onClickListener); + } return marker; } + + removeMarker(marker) { + marker.setMap(null); + } + /* eslint-enable no-undef */ /* eslint-disable no-undef */ @@ -266,6 +277,10 @@ export default class TbGoogleMap { } /* eslint-enable no-undef */ + removePolyline(polyline) { + polyline.setMap(null); + } + /* eslint-disable no-undef */ fitBounds(bounds) { if (this.dontFitMapBounds && this.defaultZoomLevel) { diff --git a/ui/src/app/widget/lib/map-widget.js b/ui/src/app/widget/lib/map-widget.js index be7b118f8c..88ef1ca848 100644 --- a/ui/src/app/widget/lib/map-widget.js +++ b/ui/src/app/widget/lib/map-widget.js @@ -19,11 +19,62 @@ import tinycolor from 'tinycolor2'; import TbGoogleMap from './google-map'; import TbOpenStreetMap from './openstreet-map'; +function procesTooltipPattern(tbMap, pattern, datasources, dsIndex) { + var match = tbMap.varsRegex.exec(pattern); + var replaceInfo = {}; + replaceInfo.variables = []; + while (match !== null) { + var variableInfo = {}; + variableInfo.dataKeyIndex = -1; + var variable = match[0]; + var label = match[1]; + var valDec = 2; + var splitVals = label.split(':'); + if (splitVals.length > 1) { + label = splitVals[0]; + valDec = parseFloat(splitVals[1]); + } + variableInfo.variable = variable; + variableInfo.valDec = valDec; + + if (label.startsWith('#')) { + var keyIndexStr = label.substring(1); + var n = Math.floor(Number(keyIndexStr)); + if (String(n) === keyIndexStr && n >= 0) { + variableInfo.dataKeyIndex = n; + } + } + if (variableInfo.dataKeyIndex === -1) { + var offset = 0; + for (var i=0;i 0 && settings.defaultZoomLevel < 21) { this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel); @@ -43,52 +97,116 @@ export default class TbMapWidget { this.dontFitMapBounds = settings.fitMapBounds === false; - function procesTooltipPattern(pattern) { - var match = tbMap.varsRegex.exec(pattern); - var replaceInfo = {}; - replaceInfo.variables = []; - while (match !== null) { - var variableInfo = {}; - variableInfo.dataKeyIndex = -1; - var variable = match[0]; - var label = match[1]; - var valDec = 2; - var splitVals = label.split(':'); - if (splitVals.length > 1) { - label = splitVals[0]; - valDec = parseFloat(splitVals[1]); + if (!useDynamicLocations) { + this.subscription = this.ctx.defaultSubscription; + this.configureLocationsFromSettings(); + } + + var minZoomLevel = this.drawRoutes ? 18 : 15; + + var initCallback = function() { + tbMap.update(); + tbMap.resize(); + }; + + if (mapProvider === 'google-map') { + this.map = new TbGoogleMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType); + } else if (mapProvider === 'openstreet-map') { + this.map = new TbOpenStreetMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel); + } + + } + + setCallbacks(callbacks) { + Object.assign(this.callbacks, callbacks); + } + + clearLocations() { + if (this.locations) { + var tbMap = this; + this.locations.forEach(function(location) { + if (location.marker) { + tbMap.map.removeMarker(location.marker); } - variableInfo.variable = variable; - variableInfo.valDec = valDec; - - if (label.startsWith('#')) { - var keyIndexStr = label.substring(1); - var n = Math.floor(Number(keyIndexStr)); - if (String(n) === keyIndexStr && n >= 0) { - variableInfo.dataKeyIndex = n; - } + if (location.polyline) { + tbMap.map.removePolyline(location.polyline); } - if (variableInfo.dataKeyIndex === -1) { - var offset = 0; - for (var i=0;i -1 && lngKeyIndex > -1) { + var locationsSettings = { + latKeyName: localLatKeyName, + lngKeyName: localLngKeyName, + showLabel: subscriptionLocationSettings.showLabel !== false, + displayTooltip: subscriptionLocationSettings.displayTooltip !== false, + label: datasource.name, + labelColor: subscriptionLocationSettings.labelColor || this.ctx.widgetConfig.color || '#000000', + color: "#FE7569", + useColorFunction: false, + colorFunction: null, + markerImage: null, + markerImageSize: 34, + useMarkerImage: false, + useMarkerImageFunction: false, + markerImageFunction: null, + markerImages: [], + tooltipPattern: subscriptionLocationSettings.tooltipPattern || "Latitude: ${latitude:7}
Longitude: ${longitude:7}" + }; + + locationsSettings.tooltipReplaceInfo = procesTooltipPattern(this, locationsSettings.tooltipPattern, this.subscription.datasources, i); + + locationsSettings.useColorFunction = subscriptionLocationSettings.useColorFunction === true; + if (angular.isDefined(subscriptionLocationSettings.colorFunction) && subscriptionLocationSettings.colorFunction.length > 0) { + try { + locationsSettings.colorFunction = new Function('data, dsData, dsIndex', subscriptionLocationSettings.colorFunction); + } catch (e) { + locationsSettings.colorFunction = null; } - offset += datasource.dataKeys.length; } + + this.locationsSettings.push(locationsSettings); + latKeyIndex = -1; + lngKeyIndex = -1; } - replaceInfo.variables.push(variableInfo); - match = tbMap.varsRegex.exec(pattern); + index++; } - return replaceInfo; } + } - var configuredLocationsSettings = drawRoutes ? settings.routesSettings : settings.markersSettings; + configureLocationsFromSettings() { + var configuredLocationsSettings = this.drawRoutes ? this.ctx.settings.routesSettings : this.ctx.settings.markersSettings; if (!configuredLocationsSettings) { configuredLocationsSettings = []; } @@ -98,8 +216,9 @@ export default class TbMapWidget { latKeyName: "lat", lngKeyName: "lng", showLabel: true, + displayTooltip: true, label: "", - labelColor: ctx.widgetConfig.color || '#000000', + labelColor: this.ctx.widgetConfig.color || '#000000', color: "#FE7569", useColorFunction: false, colorFunction: null, @@ -112,7 +231,7 @@ export default class TbMapWidget { tooltipPattern: "Latitude: ${lat:7}
Longitude: ${lng:7}" }; - if (drawRoutes) { + if (this.drawRoutes) { this.locationsSettings[i].strokeWeight = 2; this.locationsSettings[i].strokeOpacity = 1.0; } @@ -123,7 +242,7 @@ export default class TbMapWidget { this.locationsSettings[i].tooltipPattern = configuredLocationsSettings[i].tooltipPattern || "Latitude: ${"+this.locationsSettings[i].latKeyName+":7}
Longitude: ${"+this.locationsSettings[i].lngKeyName+":7}"; - this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this.locationsSettings[i].tooltipPattern); + this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this, this.locationsSettings[i].tooltipPattern, this.subscription.datasources); this.locationsSettings[i].showLabel = configuredLocationsSettings[i].showLabel !== false; this.locationsSettings[i].label = configuredLocationsSettings[i].label || this.locationsSettings[i].label; @@ -132,7 +251,7 @@ export default class TbMapWidget { this.locationsSettings[i].useColorFunction = configuredLocationsSettings[i].useColorFunction === true; if (angular.isDefined(configuredLocationsSettings[i].colorFunction) && configuredLocationsSettings[i].colorFunction.length > 0) { try { - this.locationsSettings[i].colorFunction = new Function('data', configuredLocationsSettings[i].colorFunction); + this.locationsSettings[i].colorFunction = new Function('data, dsData, dsIndex', configuredLocationsSettings[i].colorFunction); } catch (e) { this.locationsSettings[i].colorFunction = null; } @@ -141,7 +260,7 @@ export default class TbMapWidget { this.locationsSettings[i].useMarkerImageFunction = configuredLocationsSettings[i].useMarkerImageFunction === true; if (angular.isDefined(configuredLocationsSettings[i].markerImageFunction) && configuredLocationsSettings[i].markerImageFunction.length > 0) { try { - this.locationsSettings[i].markerImageFunction = new Function('data, images', configuredLocationsSettings[i].markerImageFunction); + this.locationsSettings[i].markerImageFunction = new Function('data, images, dsData, dsIndex', configuredLocationsSettings[i].markerImageFunction); } catch (e) { this.locationsSettings[i].markerImageFunction = null; } @@ -157,26 +276,12 @@ export default class TbMapWidget { this.locationsSettings[i].markerImageSize = configuredLocationsSettings[i].markerImageSize || 34; } - if (drawRoutes) { + if (this.drawRoutes) { this.locationsSettings[i].strokeWeight = configuredLocationsSettings[i].strokeWeight || this.locationsSettings[i].strokeWeight; this.locationsSettings[i].strokeOpacity = angular.isDefined(configuredLocationsSettings[i].strokeOpacity) ? configuredLocationsSettings[i].strokeOpacity : this.locationsSettings[i].strokeOpacity; } } } - - var minZoomLevel = this.drawRoutes ? 18 : 15; - - var initCallback = function() { - tbMap.update(); - tbMap.resize(); - }; - - if (mapProvider === 'google-map') { - this.map = new TbGoogleMap(ctx.$container, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType); - } else if (mapProvider === 'openstreet-map') { - this.map = new TbOpenStreetMap(ctx.$container, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel); - } - } update() { @@ -231,22 +336,22 @@ export default class TbMapWidget { return true; } - function calculateLocationColor(settings, dataMap) { - if (settings.useColorFunction && settings.colorFunction) { + function calculateLocationColor(location, dataMap) { + if (location.settings.useColorFunction && location.settings.colorFunction) { var color = '#FE7569'; try { - color = settings.colorFunction(dataMap); + color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex); } catch (e) { color = '#FE7569'; } return tinycolor(color).toHexString(); } else { - return settings.color; + return location.settings.color; } } function updateLocationColor(location, dataMap) { - var color = calculateLocationColor(location.settings, dataMap); + var color = calculateLocationColor(location, dataMap); if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) { if (!location.settings.useMarkerImage && !location.settings.useMarkerImageFunction) { tbMap.map.updateMarkerColor(location.marker, color); @@ -258,11 +363,11 @@ export default class TbMapWidget { } } - function calculateLocationMarkerImage(settings, dataMap) { - if (settings.useMarkerImageFunction && settings.markerImageFunction) { + function calculateLocationMarkerImage(location, dataMap) { + if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) { var image = null; try { - image = settings.markerImageFunction(dataMap, settings.markerImages); + image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex); } catch (e) { image = null; } @@ -273,7 +378,7 @@ export default class TbMapWidget { } function updateLocationMarkerImage(location, dataMap) { - var image = calculateLocationMarkerImage(location.settings, dataMap); + var image = calculateLocationMarkerImage(location, dataMap); if (image != null && (!location.settings.calculatedImage || !angular.equals(location.settings.calculatedImage, image))) { tbMap.map.updateMarkerImage(location.marker, location.settings, image.url, image.size); location.settings.calculatedImage = image; @@ -306,7 +411,11 @@ export default class TbMapWidget { if (latLngs.length > 0) { var markerLocation = latLngs[latLngs.length-1]; if (!location.marker) { - location.marker = tbMap.map.createMarker(markerLocation, location.settings); + location.marker = tbMap.map.createMarker(markerLocation, location.settings, + function() { + tbMap.callbacks.onLocationClick(location); + } + ); } else { tbMap.map.setMarkerPosition(location.marker, markerLocation); } @@ -328,7 +437,9 @@ export default class TbMapWidget { lng = lngData[lngData.length-1][1]; latLng = tbMap.map.createLatLng(lat, lng); if (!location.marker) { - location.marker = tbMap.map.createMarker(latLng, location.settings); + location.marker = tbMap.map.createMarker(latLng, location.settings, function() { + tbMap.callbacks.onLocationClick(location); + }); tbMap.markers.push(location.marker); locationChanged = true; } else { @@ -345,8 +456,12 @@ export default class TbMapWidget { return locationChanged; } - function toLabelValueMap(data) { + function toLabelValueMap(data, datasources) { var dataMap = {}; + var dsDataMap = []; + for (var d=0;d -1 && lngIndex > -1) { + var ds = data[latIndex].datasource; + var dsIndex = datasources.indexOf(ds); var location = { latIndex: latIndex, lngIndex: lngIndex, + dsIndex: dsIndex, settings: locationSettings }; tbMap.locations.push(location); @@ -394,10 +523,10 @@ export default class TbMapWidget { tbMap.map.fitBounds(bounds); } - function updateLocations(data) { + function updateLocations(data, datasources) { var locationsChanged = false; var bounds = tbMap.map.createBounds(); - var dataMap = toLabelValueMap(data); + var dataMap = toLabelValueMap(data, datasources); for (var p = 0; p < tbMap.locations.length; p++) { var location = tbMap.locations[p]; locationsChanged |= updateLocation(location, data, dataMap); @@ -412,36 +541,36 @@ export default class TbMapWidget { } } - if (this.map && this.map.inited()) { - if (this.ctx.data) { + if (this.map && this.map.inited() && this.subscription) { + if (this.subscription.data) { if (!this.locations) { - loadLocations(this.ctx.data); + loadLocations(this.subscription.data, this.subscription.datasources); } else { - updateLocations(this.ctx.data); + updateLocations(this.subscription.data, this.subscription.datasources); } - } - var tooltips = this.map.getTooltips(); - for (var t=0; t < tooltips.length; t++) { - var tooltip = tooltips[t]; - var text = tooltip.pattern; - var replaceInfo = tooltip.replaceInfo; - for (var v = 0; v < replaceInfo.variables.length; v++) { - var variableInfo = replaceInfo.variables[v]; - var txtVal = ''; - if (variableInfo.dataKeyIndex > -1) { - var varData = this.ctx.data[variableInfo.dataKeyIndex].data; - if (varData.length > 0) { - var val = varData[varData.length - 1][1]; - if (isNumber(val)) { - txtVal = padValue(val, variableInfo.valDec, 0); - } else { - txtVal = val; + var tooltips = this.map.getTooltips(); + for (var t=0; t < tooltips.length; t++) { + var tooltip = tooltips[t]; + var text = tooltip.pattern; + var replaceInfo = tooltip.replaceInfo; + for (var v = 0; v < replaceInfo.variables.length; v++) { + var variableInfo = replaceInfo.variables[v]; + var txtVal = ''; + if (variableInfo.dataKeyIndex > -1 && this.subscription.data[variableInfo.dataKeyIndex]) { + var varData = this.subscription.data[variableInfo.dataKeyIndex].data; + if (varData.length > 0) { + var val = varData[varData.length - 1][1]; + if (isNumber(val)) { + txtVal = padValue(val, variableInfo.valDec, 0); + } else { + txtVal = val; + } } } + text = text.split(variableInfo.variable).join(txtVal); } - text = text.split(variableInfo.variable).join(txtVal); + tooltip.popup.setContent(text); } - tooltip.popup.setContent(text); } } } @@ -449,7 +578,7 @@ export default class TbMapWidget { resize() { if (this.map && this.map.inited()) { this.map.invalidateSize(); - if (this.locations && this.locations.size > 0) { + if (this.locations && this.locations.length > 0) { var bounds = this.map.createBounds(); for (var m = 0; m < this.markers.length; m++) { this.map.extendBoundsWithMarker(bounds, this.markers[m]); diff --git a/ui/src/app/widget/lib/openstreet-map.js b/ui/src/app/widget/lib/openstreet-map.js index aacb505c06..65f7c7f06f 100644 --- a/ui/src/app/widget/lib/openstreet-map.js +++ b/ui/src/app/widget/lib/openstreet-map.js @@ -85,7 +85,7 @@ export default class TbOpenStreetMap { testImage.src = image; } - createMarker(location, settings) { + createMarker(location, settings, onClickListener) { var height = 34; var pinColor = settings.color.substr(1); var icon = L.icon({ @@ -109,11 +109,21 @@ export default class TbOpenStreetMap { this.updateMarkerImage(marker, settings, settings.markerImage, settings.markerImageSize || 34); } - this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); + if (settings.displayTooltip) { + this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); + } + + if (onClickListener) { + marker.on('click', onClickListener); + } return marker; } + removeMarker(marker) { + this.map.removeLayer(marker); + } + createTooltip(marker, pattern, replaceInfo) { var popup = L.popup(); popup.setContent(''); @@ -145,6 +155,10 @@ export default class TbOpenStreetMap { return polyline; } + removePolyline(polyline) { + this.map.removeLayer(polyline); + } + fitBounds(bounds) { if (bounds.isValid()) { if (this.dontFitMapBounds && this.defaultZoomLevel) { diff --git a/ui/src/app/widget/lib/timeseries-table-widget.js b/ui/src/app/widget/lib/timeseries-table-widget.js new file mode 100644 index 0000000000..c55c519879 --- /dev/null +++ b/ui/src/app/widget/lib/timeseries-table-widget.js @@ -0,0 +1,325 @@ +/* + * 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 './timeseries-table-widget.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import timeseriesTableWidgetTemplate from './timeseries-table-widget.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +import tinycolor from 'tinycolor2'; +import cssjs from '../../../vendor/css.js/css'; + +export default angular.module('thingsboard.widgets.timeseriesTableWidget', []) + .directive('tbTimeseriesTableWidget', TimeseriesTableWidget) + .name; + +/*@ngInject*/ +function TimeseriesTableWidget() { + return { + restrict: "E", + scope: true, + bindToController: { + tableId: '=', + config: '=', + datasources: '=', + data: '=' + }, + controller: TimeseriesTableWidgetController, + controllerAs: 'vm', + templateUrl: timeseriesTableWidgetTemplate + }; +} + +/*@ngInject*/ +function TimeseriesTableWidgetController($element, $scope, $filter) { + var vm = this; + + vm.sources = []; + vm.sourceIndex = 0; + + $scope.$watch('vm.config', function() { + if (vm.config) { + vm.settings = vm.config.settings; + vm.widgetConfig = vm.config.widgetConfig; + initialize(); + } + }); + + function initialize() { + vm.showTimestamp = vm.settings.showTimestamp !== false; + var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; + var defaultColor = tinycolor(origColor); + var mdDark = defaultColor.setAlpha(0.87).toRgbString(); + var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); + var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); + //var mdDarkIcon = mdDarkSecondary; + var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); + + var cssString = 'table.md-table th.md-column {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + 'table.md-table th.md-column md-icon.md-sort-icon {\n'+ + 'color: ' + mdDarkDisabled + ';\n'+ + '}\n'+ + 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ + 'color: ' + mdDark + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell {\n'+ + 'color: ' + mdDark + ';\n'+ + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ + '}\n'+ + 'table.md-table td.md-cell.md-placeholder {\n'+ + 'color: ' + mdDarkDisabled + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + '.md-table-pagination {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ + '}\n'+ + '.md-table-pagination .buttons md-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}'; + + var cssParser = new cssjs(); + cssParser.testMode = false; + var namespace = 'ts-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $element.addClass(namespace); + + function hashCode(str) { + var hash = 0; + var i, char; + if (str.length === 0) return hash; + for (i = 0; i < str.length; i++) { + char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return hash; + } + } + + $scope.$watch('vm.datasources', function() { + updateDatasources(); + }); + + $scope.$on('timeseries-table-data-updated', function(event, tableId) { + if (vm.tableId == tableId) { + dataUpdated(); + } + }); + + function dataUpdated() { + for (var s=0; s < vm.sources.length; s++) { + var source = vm.sources[s]; + source.rawData = vm.data.slice(source.keyStartIndex, source.keyEndIndex); + } + updateSourceData(vm.sources[vm.sourceIndex]); + $scope.$digest(); + } + + vm.onPaginate = function(source) { + updatePage(source); + } + + vm.onReorder = function(source) { + reorder(source); + updatePage(source); + } + + vm.cellStyle = function(source, index, value) { + var style = {}; + if (index > 0) { + var styleInfo = source.ts.stylesInfo[index-1]; + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + style = styleInfo.cellStyleFunction(value); + } catch (e) { + style = {}; + } + } + } + return style; + } + + vm.cellContent = function(source, index, row, value) { + if (index === 0) { + return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss'); + } else { + var strContent = ''; + if (angular.isDefined(value)) { + strContent = ''+value; + } + var content = strContent; + var contentInfo = source.ts.contentsInfo[index-1]; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + var rowData = source.ts.rowDataTemplate; + rowData['Timestamp'] = row[0]; + for (var h=0; h < source.ts.header.length; h++) { + var headerInfo = source.ts.header[h]; + rowData[headerInfo.dataKey.name] = row[headerInfo.index]; + } + content = contentInfo.cellContentFunction(value, rowData, $filter); + } catch (e) { + content = strContent; + } + } + return content; + } + } + + $scope.$watch('vm.sourceIndex', function(newIndex, oldIndex) { + if (newIndex != oldIndex) { + updateSourceData(vm.sources[vm.sourceIndex]); + } + }); + + function updateDatasources() { + vm.sources = []; + vm.sourceIndex = 0; + var keyOffset = 0; + if (vm.datasources) { + for (var ds = 0; ds < vm.datasources.length; ds++) { + var source = {}; + var datasource = vm.datasources[ds]; + source.keyStartIndex = keyOffset; + keyOffset += datasource.dataKeys.length; + source.keyEndIndex = keyOffset; + source.datasource = datasource; + source.data = []; + source.rawData = []; + source.query = { + limit: 5, + page: 1, + order: '-0' + } + source.ts = { + header: [], + count: 0, + data: [], + stylesInfo: [], + contentsInfo: [], + rowDataTemplate: {} + } + source.ts.rowDataTemplate['Timestamp'] = null; + for (var a = 0; a < datasource.dataKeys.length; a++ ) { + var dataKey = datasource.dataKeys[a]; + var keySettings = dataKey.settings; + source.ts.header.push({ + index: a+1, + dataKey: dataKey + }); + source.ts.rowDataTemplate[dataKey.label] = null; + + var cellStyleFunction = null; + var useCellStyleFunction = false; + + if (keySettings.useCellStyleFunction === true) { + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { + try { + cellStyleFunction = new Function('value', keySettings.cellStyleFunction); + useCellStyleFunction = true; + } catch (e) { + cellStyleFunction = null; + useCellStyleFunction = false; + } + } + } + + source.ts.stylesInfo.push({ + useCellStyleFunction: useCellStyleFunction, + cellStyleFunction: cellStyleFunction + }); + + var cellContentFunction = null; + var useCellContentFunction = false; + + if (keySettings.useCellContentFunction === true) { + if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { + try { + cellContentFunction = new Function('value, rowData, filter', keySettings.cellContentFunction); + useCellContentFunction = true; + } catch (e) { + cellContentFunction = null; + useCellContentFunction = false; + } + } + } + + source.ts.contentsInfo.push({ + useCellContentFunction: useCellContentFunction, + cellContentFunction: cellContentFunction + }); + + } + vm.sources.push(source); + } + } + } + + function updatePage(source) { + var startIndex = source.query.limit * (source.query.page - 1); + source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit); + } + + function reorder(source) { + source.data = $filter('orderBy')(source.data, source.query.order); + } + + function convertData(data) { + var rowsMap = {}; + for (var d = 0; d < data.length; d++) { + var columnData = data[d].data; + for (var i = 0; i < columnData.length; i++) { + var cellData = columnData[i]; + var timestamp = cellData[0]; + var row = rowsMap[timestamp]; + if (!row) { + row = []; + row[0] = timestamp; + for (var c = 0; c < data.length; c++) { + row[c+1] = undefined; + } + rowsMap[timestamp] = row; + } + row[d+1] = cellData[1]; + } + } + var rows = []; + for (var t in rowsMap) { + rows.push(rowsMap[t]); + } + return rows; + } + + function updateSourceData(source) { + source.data = convertData(source.rawData); + source.ts.count = source.data.length; + reorder(source); + updatePage(source); + } + +} \ No newline at end of file diff --git a/ui/src/app/widget/lib/timeseries-table-widget.scss b/ui/src/app/widget/lib/timeseries-table-widget.scss new file mode 100644 index 0000000000..99a06537e9 --- /dev/null +++ b/ui/src/app/widget/lib/timeseries-table-widget.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-timeseries-table-widget { + table.md-table thead.md-head>tr.md-row { + height: 40px; + } + + table.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row { + height: 38px; + } + + .md-table-pagination>* { + height: 46px; + } +} diff --git a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html new file mode 100644 index 0000000000..2b6d72c10a --- /dev/null +++ b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + +
Timestamp{{ h.dataKey.label }}
+
+
+ + +
+
\ No newline at end of file diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index 9f89aba308..ab9b0d7d73 100644 --- a/ui/src/scss/main.scss +++ b/ui/src/scss/main.scss @@ -236,6 +236,32 @@ div { } } +pre.tb-highlight { + background-color: #f7f7f7; + display: block; + margin: 20px 0; + padding: 15px; + overflow-x: auto; + code { + padding: 0; + color: #303030; + font-family: monospace; + display: inline-block; + box-sizing: border-box; + vertical-align: bottom; + font-size: 16px; + font-weight: bold; + } +} + +.tb-notice { + background-color: #f7f7f7; + padding: 15px; + border: 1px solid #ccc; + font-size: 16px; +} + + /*********************** * Flow ***********************/ diff --git a/ui/webpack.config.dev.js b/ui/webpack.config.dev.js index c669ec5b22..fa019d3292 100644 --- a/ui/webpack.config.dev.js +++ b/ui/webpack.config.dev.js @@ -60,6 +60,7 @@ module.exports = { allChunks: true, }), new webpack.DefinePlugin({ + THINGSBOARD_VERSION: JSON.stringify(require('./package.json').version), '__DEVTOOLS__': false, 'process.env': { NODE_ENV: JSON.stringify('development'), diff --git a/ui/webpack.config.prod.js b/ui/webpack.config.prod.js index 09374fb6a5..a746488850 100644 --- a/ui/webpack.config.prod.js +++ b/ui/webpack.config.prod.js @@ -58,6 +58,7 @@ module.exports = { allChunks: true, }), new webpack.DefinePlugin({ + THINGSBOARD_VERSION: JSON.stringify(require('./package.json').version), '__DEVTOOLS__': false, 'process.env': { NODE_ENV: JSON.stringify('production'),