thingsboard-memoizeit
Changes
application/pom.xml 2(+1 -1)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java 199(+141 -58)
application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java 17(+16 -1)
application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java 4(+2 -2)
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java 194(+194 -0)
application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java 6(+5 -1)
application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java 15(+11 -4)
common/data/pom.xml 2(+1 -1)
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdDeserializer.java 43(+43 -0)
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java 103(+103 -0)
common/message/pom.xml 2(+1 -1)
common/pom.xml 2(+1 -1)
common/transport/pom.xml 2(+1 -1)
dao/pom.xml 2(+1 -1)
dao/src/main/resources/schema.cql 51(+51 -0)
docker/docker-compose.yml 4(+2 -2)
docker/zookeeper/zkOk.sh 2(+1 -1)
extensions/extension-kafka/pom.xml 2(+1 -1)
extensions/pom.xml 2(+1 -1)
extensions-api/pom.xml 2(+1 -1)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java 5(+5 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java 26(+13 -13)
extensions-core/pom.xml 7(+6 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java 3(+2 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java 5(+3 -2)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java 29(+29 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java 287(+172 -115)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java 110(+74 -36)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 58(+30 -28)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java 5(+3 -2)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java 11(+5 -6)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java 78(+48 -30)
pom.xml 2(+1 -1)
tools/pom.xml 2(+1 -1)
transport/coap/pom.xml 2(+1 -1)
transport/http/pom.xml 2(+1 -1)
transport/mqtt/pom.xml 2(+1 -1)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java 7(+6 -1)
transport/pom.xml 2(+1 -1)
ui/package.json 2(+1 -1)
ui/pom.xml 2(+1 -1)
ui/src/app/api/asset.service.js 261(+261 -0)
ui/src/app/api/attribute.service.js 282(+282 -0)
ui/src/app/api/dashboard.service.js 35(+34 -1)
ui/src/app/api/datasource.service.js 25(+14 -11)
ui/src/app/api/device.service.js 382(+24 -358)
ui/src/app/api/entity.service.js 777(+777 -0)
ui/src/app/api/entity-relation.service.js 136(+136 -0)
ui/src/app/api/subscription.js 123(+67 -56)
ui/src/app/api/user.service.js 15(+11 -4)
ui/src/app/app.config.js 2(+1 -1)
ui/src/app/app.js 10(+10 -0)
ui/src/app/asset/add-asset.tpl.html 45(+45 -0)
ui/src/app/asset/asset.controller.js 506(+506 -0)
ui/src/app/asset/asset.directive.js 71(+71 -0)
ui/src/app/asset/asset.routes.js 68(+68 -0)
ui/src/app/asset/asset-card.tpl.html 19(+19 -0)
ui/src/app/asset/asset-fieldset.tpl.html 71(+71 -0)
ui/src/app/asset/assets.tpl.html 58(+58 -0)
ui/src/app/asset/assign-to-customer.controller.js 123(+123 -0)
ui/src/app/asset/assign-to-customer.tpl.html 76(+76 -0)
ui/src/app/asset/index.js 43(+43 -0)
ui/src/app/common/dashboard-utils.service.js 437(+437 -0)
ui/src/app/common/types.constant.js 22(+17 -5)
ui/src/app/common/utils.service.js 162(+34 -128)
ui/src/app/components/dashboard.directive.js 191(+165 -26)
ui/src/app/components/datasource-entity.directive.js 249(+249 -0)
ui/src/app/components/datasource-entity.scss 44(+44 -0)
ui/src/app/components/datasource-entity.tpl.html 137(+137 -0)
ui/src/app/components/widget.controller.js 26(+19 -7)
ui/src/app/components/widget-config.directive.js 217(+122 -95)
ui/src/app/customer/customer.controller.js 26(+25 -1)
ui/src/app/customer/customers.tpl.html 44(+37 -7)
ui/src/app/dashboard/add-widget.controller.js 53(+31 -22)
ui/src/app/dashboard/dashboard.controller.js 743(+502 -241)
ui/src/app/dashboard/dashboard.scss 36(+35 -1)
ui/src/app/dashboard/dashboard.tpl.html 241(+130 -111)
ui/src/app/dashboard/dashboard-settings.tpl.html 215(+121 -94)
ui/src/app/dashboard/edit-widget.directive.js 49(+30 -19)
ui/src/app/dashboard/index.js 18(+7 -11)
ui/src/app/dashboard/layouts/index.js 25(+25 -0)
ui/src/app/dashboard/states/index.js 29(+29 -0)
ui/src/app/device/devices.tpl.html 12(+7 -5)
ui/src/app/device/index.js 6(+0 -6)
ui/src/app/entity/aliases-entity-select.directive.js 154(+154 -0)
ui/src/app/entity/aliases-entity-select.scss 46(+46 -0)
ui/src/app/entity/attribute/attribute-table.tpl.html 210(+210 -0)
ui/src/app/entity/entity-aliases.controller.js 221(+221 -0)
ui/src/app/entity/entity-aliases.scss 28(+28 -0)
ui/src/app/entity/entity-aliases.tpl.html 106(+106 -0)
ui/src/app/entity/entity-filter.directive.js 234(+234 -0)
ui/src/app/entity/entity-filter.scss 45(+45 -0)
ui/src/app/entity/entity-filter.tpl.html 67(+67 -0)
ui/src/app/entity/entity-type-select.directive.js 120(+120 -0)
ui/src/app/entity/entity-type-select.scss 18(+18 -0)
ui/src/app/entity/index.js 35(+35 -0)
ui/src/app/import-export/import-export.service.js 200(+131 -69)
ui/src/app/layout/index.js 4(+4 -0)
ui/src/app/locale/locale.constant.js 157(+148 -9)
ui/src/app/locale/locale.constant-zh.js 150(+75 -75)
ui/src/app/locale/translate-handler.js 26(+26 -0)
ui/src/app/plugin/plugin-fieldset.tpl.html 10(+10 -0)
ui/src/app/plugin/plugins.tpl.html 17(+17 -0)
ui/src/app/rule/rule.directive.js 7(+6 -1)
ui/src/app/rule/rule-fieldset.tpl.html 10(+10 -0)
ui/src/app/rule/rules.tpl.html 17(+17 -0)
ui/src/app/services/item-buffer.service.js 272(+173 -99)
ui/src/app/services/menu.service.js 32(+32 -0)
ui/src/app/tenant/tenant-fieldset.tpl.html 10(+10 -0)
ui/src/app/tenant/tenants.tpl.html 40(+35 -5)
ui/src/app/user/user.controller.js 12(+9 -3)
ui/src/app/user/user.directive.js 4(+4 -0)
ui/src/app/user/user-fieldset.tpl.html 13(+11 -2)
ui/src/scss/main.scss 18(+17 -1)
Details
application/pom.xml 2(+1 -1)
diff --git a/application/pom.xml b/application/pom.xml
index f3fd1d0..9364cfc 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
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 7cc2c9f..ae9d967 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;
@@ -81,6 +82,9 @@ public class ActorSystemContext {
@Getter private DeviceService deviceService;
@Autowired
+ @Getter private AssetService assetService;
+
+ @Autowired
@Getter private TenantService tenantService;
@Autowired
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 876b525..5dbc5f4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.actors.plugin;
import java.io.IOException;
import java.util.*;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
@@ -30,15 +29,19 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
-import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
@@ -53,7 +56,6 @@ import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRe
import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
import akka.actor.ActorRef;
-import org.w3c.dom.Attr;
import javax.annotation.Nullable;
@@ -91,103 +93,107 @@ public final class PluginProcessingContext implements PluginContext {
}
@Override
- public void saveAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List<AttributeKvEntry> attributes, final PluginCallback<Void> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes);
+ public void saveAttributes(final TenantId tenantId, final EntityId entityId, final String scope, final List<AttributeKvEntry> attributes, final PluginCallback<Void> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<List<ResultSet>> 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<String> keys, final PluginCallback<Void> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<List<ResultSet>> future = pluginCtx.attributesService.removeAll(deviceId, scope, keys);
+ public void removeAttributes(final TenantId tenantId, final EntityId entityId, final String scope, final List<String> keys, final PluginCallback<Void> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<List<ResultSet>> 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<Optional<AttributeKvEntry>> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<Optional<AttributeKvEntry>> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
+ public void loadAttribute(EntityId entityId, String attributeType, String attributeKey, final PluginCallback<Optional<AttributeKvEntry>> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<Optional<AttributeKvEntry>> future = pluginCtx.attributesService.find(entityId, attributeType, attributeKey);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
}
@Override
- public void loadAttributes(DeviceId deviceId, String attributeType, Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys);
+ public void loadAttributes(EntityId entityId, String attributeType, Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.find(entityId, attributeType, attributeKeys);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
}
@Override
- public void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.findAll(deviceId, attributeType);
+ public void loadAttributes(EntityId entityId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.findAll(entityId, attributeType);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
}
@Override
- public void loadAttributes(final DeviceId deviceId, final Collection<String> attributeTypes, final PluginCallback<List<AttributeKvEntry>> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
+ public void loadAttributes(final EntityId entityId, final Collection<String> attributeTypes, final PluginCallback<List<AttributeKvEntry>> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
List<ListenableFuture<List<AttributeKvEntry>>> 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<String> attributeTypes, final Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
+ public void loadAttributes(final EntityId entityId, final Collection<String> attributeTypes, final Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
List<ListenableFuture<List<AttributeKvEntry>>> 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<Void> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry);
+ public void saveTsData(final EntityId entityId, final TsKvEntry entry, final PluginCallback<Void> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(entityId, entry);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
}));
}
@Override
- public void saveTsData(final DeviceId deviceId, final List<TsKvEntry> entries, final PluginCallback<Void> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries);
+ public void saveTsData(final EntityId entityId, final List<TsKvEntry> entries, final PluginCallback<Void> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(entityId, entries);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
}));
}
@Override
- public void loadTimeseries(final DeviceId deviceId, final List<TsKvQuery> queries, final PluginCallback<List<TsKvEntry>> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<List<TsKvEntry>> future = pluginCtx.tsService.findAll(DataConstants.DEVICE, deviceId, queries);
+ public void loadTimeseries(final EntityId entityId, final List<TsKvQuery> queries, final PluginCallback<List<TsKvEntry>> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<List<TsKvEntry>> future = pluginCtx.tsService.findAll(entityId, queries);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
}
@Override
- public void loadLatestTimeseries(final DeviceId deviceId, final PluginCallback<List<TsKvEntry>> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId);
+ public void loadLatestTimeseries(final EntityId entityId, final PluginCallback<List<TsKvEntry>> 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<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
- validate(deviceId, new ValidationCallback(callback, ctx -> {
- ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.findLatest(DataConstants.DEVICE, deviceId, keys);
+ public void loadLatestTimeseries(final EntityId entityId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
+ validate(entityId, new ValidationCallback(callback, ctx -> {
+ ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.findLatest(entityId, keys);
Futures.addCallback(rsListFuture, getListCallback(callback, rsList ->
{
List<TsKvEntry> result = new ArrayList<>();
@@ -270,24 +276,101 @@ public final class PluginProcessingContext implements PluginContext {
validate(deviceId, new ValidationCallback(callback, ctx -> callback.onSuccess(ctx, null)));
}
- private void validate(DeviceId deviceId, ValidationCallback callback) {
+ private void validate(EntityId entityId, ValidationCallback callback) {
if (securityCtx.isPresent()) {
final PluginApiCallSecurityContext ctx = securityCtx.get();
- if (ctx.isTenantAdmin() || ctx.isCustomerUser()) {
- ListenableFuture<Device> 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<Device> 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<Asset> 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<RuleMetaData> 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<PluginMetaData> 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<Customer> 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<Tenant> tenantFuture = pluginCtx.tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
+ Futures.addCallback(tenantFuture, getCallback(callback, tenant -> tenant != null && tenant.getId().equals(ctx.getTenantId())));
+ }
+ return;
+ default:
+ //TODO: add support of other entities
+ throw new IllegalStateException("Not Implemented!");
+ }
} else {
callback.onSuccess(this, Boolean.FALSE);
}
@@ -297,8 +380,8 @@ public final class PluginProcessingContext implements PluginContext {
}
@Override
- public Optional<ServerAddress> resolve(DeviceId deviceId) {
- return pluginCtx.routingService.resolve(deviceId);
+ public Optional<ServerAddress> 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 b09b72e..43ddce6 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 <T> void forward(DeviceId deviceId, T msg, BiConsumer<ServerAddress, T> rpcFunction) {
- Optional<ServerAddress> instance = routingService.resolve(deviceId);
+ Optional<ServerAddress> 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 707afa5..c8d79b4 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<Boolean> {
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 54160bc..65d2c69 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<ServerAddress> address = actorContext.getRoutingService().resolve(deviceId);
+ Optional<ServerAddress> 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/AbstractSessionActorMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
index b8d13ec..483c034 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<ServerAddress> forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward) {
- Optional<ServerAddress> address = systemContext.getRoutingService().resolve(toForward.getDeviceId());
+ Optional<ServerAddress> address = systemContext.getRoutingService().resolveById(toForward.getDeviceId());
forwardToAppActor(ctx, toForward, address);
return address;
}
protected Optional<ServerAddress> forwardToAppActorIfAdressChanged(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> oldAddress) {
- Optional<ServerAddress> newAddress = systemContext.getRoutingService().resolve(toForward.getDeviceId());
+ Optional<ServerAddress> 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/actors/session/ASyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
index f806687..b451d77 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<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
+ Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolveById(getDeviceId());
if (!newTargetServer.equals(currentTargetServer)) {
firstMsg = true;
currentTargetServer = newTargetServer;
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 0000000..24497d4
--- /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<Asset> 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<Asset> 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<Asset> getAssetsByIds(
+ @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException {
+ checkArrayParameter("assetIds", strAssetIds);
+ try {
+ SecurityUser user = getCurrentUser();
+ TenantId tenantId = user.getTenantId();
+ CustomerId customerId = user.getCustomerId();
+ List<AssetId> assetIds = new ArrayList<>();
+ for (String strAssetId : strAssetIds) {
+ assetIds.add(new AssetId(toUUID(strAssetId)));
+ }
+ ListenableFuture<List<Asset>> 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<Asset> findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException {
+ checkNotNull(query);
+ checkNotNull(query.getParameters());
+ checkNotNull(query.getAssetTypes());
+ checkEntityId(query.getParameters().getEntityId());
+ try {
+ List<Asset> assets = checkNotNull(assetService.findAssetsByQuery(query).get());
+ assets = assets.stream().filter(asset -> {
+ try {
+ checkAsset(asset);
+ return true;
+ } catch (ThingsboardException e) {
+ return false;
+ }
+ }).collect(Collectors.toList());
+ return assets;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 1b6696f..d4adebe 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -24,10 +24,8 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.thingsboard.server.actors.service.ActorService;
-import org.thingsboard.server.common.data.Customer;
-import org.thingsboard.server.common.data.Dashboard;
-import org.thingsboard.server.common.data.Device;
-import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
@@ -38,6 +36,7 @@ import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
@@ -46,6 +45,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
@@ -81,6 +81,9 @@ public abstract class BaseController {
protected DeviceService deviceService;
@Autowired
+ protected AssetService assetService;
+
+ @Autowired
protected DeviceCredentialsService deviceCredentialsService;
@Autowired
@@ -104,6 +107,9 @@ public abstract class BaseController {
@Autowired
protected ActorService actorService;
+ @Autowired
+ protected RelationService relationService;
+
@ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
@@ -253,6 +259,43 @@ public abstract class BaseController {
}
}
+ protected void checkEntityId(EntityId entityId) throws ThingsboardException {
+ try {
+ checkNotNull(entityId);
+ validateId(entityId.getId(), "Incorrect entityId " + entityId);
+ switch (entityId.getEntityType()) {
+ case DEVICE:
+ checkDevice(deviceService.findDeviceById(new DeviceId(entityId.getId())));
+ return;
+ case CUSTOMER:
+ checkCustomerId(new CustomerId(entityId.getId()));
+ return;
+ case TENANT:
+ checkTenantId(new TenantId(entityId.getId()));
+ return;
+ case PLUGIN:
+ checkPlugin(new PluginId(entityId.getId()));
+ return;
+ case RULE:
+ checkRule(new RuleId(entityId.getId()));
+ return;
+ case ASSET:
+ checkAsset(assetService.findAssetById(new AssetId(entityId.getId())));
+ return;
+ case DASHBOARD:
+ checkDashboardId(new DashboardId(entityId.getId()));
+ return;
+ case USER:
+ checkUserId(new UserId(entityId.getId()));
+ return;
+ default:
+ throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType());
+ }
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
Device checkDeviceId(DeviceId deviceId) throws ThingsboardException {
try {
validateId(deviceId, "Incorrect deviceId " + deviceId);
@@ -264,7 +307,7 @@ public abstract class BaseController {
}
}
- private void checkDevice(Device device) throws ThingsboardException {
+ protected void checkDevice(Device device) throws ThingsboardException {
checkNotNull(device);
checkTenantId(device.getTenantId());
if (device.getCustomerId() != null && !device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
@@ -272,6 +315,25 @@ public abstract class BaseController {
}
}
+ Asset checkAssetId(AssetId assetId) throws ThingsboardException {
+ try {
+ validateId(assetId, "Incorrect assetId " + assetId);
+ Asset asset = assetService.findAssetById(assetId);
+ checkAsset(asset);
+ return asset;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ protected void checkAsset(Asset asset) throws ThingsboardException {
+ checkNotNull(asset);
+ checkTenantId(asset.getTenantId());
+ if (asset.getCustomerId() != null && !asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+ checkCustomerId(asset.getCustomerId());
+ }
+ }
+
WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, boolean modify) throws ThingsboardException {
try {
validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
@@ -318,14 +380,26 @@ public abstract class BaseController {
try {
validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
Dashboard dashboard = dashboardService.findDashboardById(dashboardId);
- checkDashboard(dashboard);
+ checkDashboard(dashboard, true);
return dashboard;
} catch (Exception e) {
throw handleException(e, false);
}
}
- private void checkDashboard(Dashboard dashboard) throws ThingsboardException {
+ DashboardInfo checkDashboardInfoId(DashboardId dashboardId) throws ThingsboardException {
+ try {
+ validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+ DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(dashboardId);
+ SecurityUser authUser = getCurrentUser();
+ checkDashboard(dashboardInfo, authUser.getAuthority() != Authority.SYS_ADMIN);
+ return dashboardInfo;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ private void checkDashboard(DashboardInfo dashboard, boolean checkCustomerId) throws ThingsboardException {
checkNotNull(dashboard);
checkTenantId(dashboard.getTenantId());
SecurityUser authUser = getCurrentUser();
@@ -335,7 +409,8 @@ public abstract class BaseController {
ThingsboardErrorCode.PERMISSION_DENIED);
}
}
- if (dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+ if (checkCustomerId &&
+ dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
checkCustomerId(dashboard.getCustomerId());
}
}
@@ -387,6 +462,16 @@ public abstract class BaseController {
return plugin;
}
+ protected PluginMetaData checkPlugin(PluginId pluginId) throws ThingsboardException {
+ checkNotNull(pluginId);
+ return checkPlugin(pluginService.findPluginById(pluginId));
+ }
+
+ protected RuleMetaData checkRule(RuleId ruleId) throws ThingsboardException {
+ checkNotNull(ruleId);
+ return checkRule(ruleService.findRuleById(ruleId));
+ }
+
protected RuleMetaData checkRule(RuleMetaData rule) throws ThingsboardException {
checkNotNull(rule);
SecurityUser authUser = getCurrentUser();
@@ -412,7 +497,8 @@ public abstract class BaseController {
if (request.getHeader("x-forwarded-port") != null) {
try {
serverPort = request.getIntHeader("x-forwarded-port");
- } catch (NumberFormatException e) {}
+ } catch (NumberFormatException e) {
+ }
}
String baseUrl = String.format("%s://%s:%d",
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index 3812610..2a6416c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -41,6 +41,19 @@ public class DashboardController extends BaseController {
return System.currentTimeMillis();
}
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET)
+ @ResponseBody
+ public DashboardInfo getDashboardInfoById(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
+ checkParameter("dashboardId", strDashboardId);
+ try {
+ DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
+ return checkDashboardInfoId(dashboardId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
@ResponseBody
@@ -132,6 +145,25 @@ public class DashboardController extends BaseController {
}
}
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<DashboardInfo> getTenantDashboards(
+ @PathVariable("tenantId") String strTenantId,
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ checkTenantId(tenantId);
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
@ResponseBody
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index bebab8b..7cd381c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -24,19 +24,18 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.dao.device.DeviceSearchQuery;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.List;
-import java.util.UUID;
+import java.util.stream.Collectors;
@RestController
@RequestMapping("/api")
@@ -238,4 +237,28 @@ public class DeviceController extends BaseController {
throw handleException(e);
}
}
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/devices", method = RequestMethod.POST)
+ @ResponseBody
+ public List<Device> findByQuery(@RequestBody DeviceSearchQuery query) throws ThingsboardException {
+ checkNotNull(query);
+ checkNotNull(query.getParameters());
+ checkNotNull(query.getDeviceTypes());
+ checkEntityId(query.getParameters().getEntityId());
+ try {
+ List<Device> devices = checkNotNull(deviceService.findDevicesByQuery(query).get());
+ devices = devices.stream().filter(device -> {
+ try {
+ checkDevice(device);
+ return true;
+ } catch (ThingsboardException e) {
+ return false;
+ }
+ }).collect(Collectors.toList());
+ return devices;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
new file mode 100644
index 0000000..0c1fd8b
--- /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<EntityRelation> 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<EntityRelation> 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<EntityRelation> 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<EntityRelation> 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<EntityRelation> 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 1bf6aa9..dc35324 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 089ade8..c0efbfc 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<ServerAddress> resolve(UUIDBased entityId);
+ Optional<ServerAddress> resolveByUuid(UUID uuid);
+
+ Optional<ServerAddress> 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 b57e15e..c29833b 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<ServerAddress> resolve(UUIDBased entityId) {
- Assert.notNull(entityId);
+ public Optional<ServerAddress> resolveById(EntityId entityId) {
+ return resolveByUuid(entityId.getId());
+ }
+
+ @Override
+ public Optional<ServerAddress> 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<Long, ServerInstance> tailMap =
circle.tailMap(hash);
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index e64e9cd..69b1cd7 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -60,8 +60,8 @@ plugins:
# JWT Token parameters
security.jwt:
- tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins)
- refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour)
+ tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000000}" # Number of seconds (15 mins)
+ refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:36000000}" # Seconds (1 hour)
tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
diff --git a/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
index 2940a62..857e083 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<TsKvEntry> 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);
}
}
common/data/pom.xml 2(+1 -1)
diff --git a/common/data/pom.xml b/common/data/pom.xml
index aca4c9c..ffee17c 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
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 0000000..19d671c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.alarm;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.EntityId;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+@Data
+public class Alarm extends BaseData<AlarmId> {
+
+ private long startTs;
+ private long endTs;
+ private long ackTs;
+ private long clearTs;
+ private String type;
+ private EntityId originator;
+ private AlarmSeverity severity;
+ private AlarmStatus status;
+ private JsonNode details;
+ private boolean propagate;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java
new file mode 100644
index 0000000..ad0fd5b
--- /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 0000000..04d23a5
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.alarm;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+@Data
+public class AlarmQuery {
+
+ private EntityId affectedEntityId;
+ private TimePageLink pageLink;
+ private AlarmStatus status;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java
new file mode 100644
index 0000000..ed73027
--- /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 0000000..54fbc9d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.alarm;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+public enum AlarmStatus {
+
+ ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
new file mode 100644
index 0000000..2e20b77
--- /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<AssetId> {
+
+ 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/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
index 6b31283..92e8655 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -28,6 +28,7 @@ public class Device extends SearchTextBased<DeviceId> {
private TenantId tenantId;
private CustomerId customerId;
private String name;
+ private String type;
private JsonNode additionalInfo;
public Device() {
@@ -43,6 +44,7 @@ public class Device extends SearchTextBased<DeviceId> {
this.tenantId = device.getTenantId();
this.customerId = device.getCustomerId();
this.name = device.getName();
+ this.type = device.getType();
this.additionalInfo = device.getAdditionalInfo();
}
@@ -70,6 +72,14 @@ public class Device extends SearchTextBased<DeviceId> {
this.name = name;
}
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
public JsonNode getAdditionalInfo() {
return additionalInfo;
}
@@ -90,6 +100,7 @@ public class Device extends SearchTextBased<DeviceId> {
result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
return result;
}
@@ -118,6 +129,11 @@ public class Device extends SearchTextBased<DeviceId> {
return false;
} else if (!name.equals(other.name))
return false;
+ if (type == null) {
+ if (other.type != null)
+ return false;
+ } else if (!type.equals(other.type))
+ return false;
if (tenantId == null) {
if (other.tenantId != null)
return false;
@@ -135,6 +151,8 @@ public class Device extends SearchTextBased<DeviceId> {
builder.append(customerId);
builder.append(", name=");
builder.append(name);
+ builder.append(", type=");
+ builder.append(type);
builder.append(", additionalInfo=");
builder.append(additionalInfo);
builder.append(", createdTime=");
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
index 8dac8f5..ebf42f2 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/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java
new file mode 100644
index 0000000..049116e
--- /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 632d5c2..a9a3441 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 f0caec5..17d69ac 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 0000000..f9f34c4
--- /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<EntityId> {
+
+ @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 0000000..21093ce
--- /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 0000000..18ffaea
--- /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<EntityId> {
+
+ @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 51b1140..31b6642 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 0000000..8fcf269
--- /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);
+ }
+}
common/message/pom.xml 2(+1 -1)
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 01d3b45..5362f68 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
common/pom.xml 2(+1 -1)
diff --git a/common/pom.xml b/common/pom.xml
index 9c7b51e..798bec4 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
common/transport/pom.xml 2(+1 -1)
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index 4a2bb52..c537756 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
dao/pom.xml 2(+1 -1)
diff --git a/dao/pom.xml b/dao/pom.xml
index 146b8c1..cc3804f 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
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 0000000..a116598
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.alarm;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+public interface AlarmDao {
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
new file mode 100644
index 0000000..35bd247
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.alarm;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.page.TimePageData;
+
+import java.util.Optional;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+public interface AlarmService {
+
+ Optional<Alarm> saveIfNotExists(Alarm alarm);
+
+ ListenableFuture<Boolean> updateAlarm(Alarm alarm);
+
+ ListenableFuture<Boolean> ackAlarm(Alarm alarm);
+
+ ListenableFuture<Boolean> clearAlarm(AlarmId alarmId);
+
+ ListenableFuture<TimePageData<Alarm>> findAlarms(AlarmQuery query);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
new file mode 100644
index 0000000..81f95b4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.asset;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.AssetEntity;
+import org.thingsboard.server.dao.model.DeviceEntity;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * The Interface AssetDao.
+ *
+ */
+public interface AssetDao extends Dao<AssetEntity> {
+
+ /**
+ * Save or update asset object
+ *
+ * @param asset the asset object
+ * @return saved asset object
+ */
+ AssetEntity save(Asset asset);
+
+ /**
+ * Find assets by tenantId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param pageLink the page link
+ * @return the list of asset objects
+ */
+ List<AssetEntity> 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<List<AssetEntity>> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List<UUID> 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<AssetEntity> 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<List<AssetEntity>> findAssetsByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> assetIds);
+
+ /**
+ * Find assets by tenantId and asset name.
+ *
+ * @param tenantId the tenantId
+ * @param name the asset name
+ * @return the optional asset object
+ */
+ Optional<AssetEntity> 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 0000000..ed08c6d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.asset;
+
+import com.datastax.driver.core.querybuilder.Select;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.AssetEntity;
+
+import java.util.*;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Component
+@Slf4j
+public class AssetDaoImpl extends AbstractSearchTextDao<AssetEntity> implements AssetDao {
+
+ @Override
+ protected Class<AssetEntity> getColumnFamilyClass() {
+ return AssetEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ASSET_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public AssetEntity save(Asset asset) {
+ log.debug("Save asset [{}] ", asset);
+ return save(new AssetEntity(asset));
+ }
+
+ @Override
+ public List<AssetEntity> findAssetsByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find assets by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<AssetEntity> assetEntities = findPageWithTextSearch(ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Collections.singletonList(eq(ASSET_TENANT_ID_PROPERTY, tenantId)), pageLink);
+
+ log.trace("Found assets [{}] by tenantId [{}] and pageLink [{}]", assetEntities, tenantId, pageLink);
+ return assetEntities;
+ }
+
+ @Override
+ public ListenableFuture<List<AssetEntity>> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List<UUID> 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<AssetEntity> findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
+ log.debug("Try to find assets by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
+ List<AssetEntity> assetEntities = findPageWithTextSearch(ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(ASSET_CUSTOMER_ID_PROPERTY, customerId),
+ eq(ASSET_TENANT_ID_PROPERTY, tenantId)),
+ pageLink);
+
+ log.trace("Found assets [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", assetEntities, tenantId, customerId, pageLink);
+ return assetEntities;
+ }
+
+ @Override
+ public ListenableFuture<List<AssetEntity>> findAssetsByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> 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<AssetEntity> findAssetsByTenantIdAndName(UUID tenantId, String assetName) {
+ Select select = select().from(ASSET_BY_TENANT_AND_NAME_VIEW_NAME);
+ Select.Where query = select.where();
+ query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId));
+ query.and(eq(ASSET_NAME_PROPERTY, assetName));
+ return Optional.ofNullable(findOneByStatement(query));
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java
new file mode 100644
index 0000000..f3c69b2
--- /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<String> 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 0000000..7a61bd8
--- /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<Asset> findAssetByIdAsync(AssetId assetId);
+
+ Optional<Asset> findAssetByTenantIdAndName(TenantId tenantId, String name);
+
+ Asset saveAsset(Asset device);
+
+ Asset assignAssetToCustomer(AssetId assetId, CustomerId customerId);
+
+ Asset unassignAssetFromCustomer(AssetId assetId);
+
+ void deleteAsset(AssetId assetId);
+
+ TextPageData<Asset> findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink);
+
+ ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds);
+
+ void deleteAssetsByTenantId(TenantId tenantId);
+
+ TextPageData<Asset> findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
+
+ ListenableFuture<List<Asset>> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<AssetId> assetIds);
+
+ void unassignCustomerAssets(TenantId tenantId, CustomerId customerId);
+
+ ListenableFuture<List<Asset>> 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 0000000..221e4df
--- /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<String> 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 0000000..c63e5d7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
@@ -0,0 +1,292 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.asset;
+
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.AssetEntity;
+import org.thingsboard.server.dao.model.CustomerEntity;
+import org.thingsboard.server.dao.model.TenantEntity;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.tenant.TenantDao;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.server.dao.DaoUtil.*;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.thingsboard.server.dao.service.Validator.*;
+
+@Service
+@Slf4j
+public class BaseAssetService extends BaseEntityService implements AssetService {
+
+ @Autowired
+ private AssetDao assetDao;
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Autowired
+ private CustomerDao customerDao;
+
+ @Override
+ public Asset findAssetById(AssetId assetId) {
+ log.trace("Executing findAssetById [{}]", assetId);
+ validateId(assetId, "Incorrect assetId " + assetId);
+ AssetEntity assetEntity = assetDao.findById(assetId.getId());
+ return getData(assetEntity);
+ }
+
+ @Override
+ public ListenableFuture<Asset> findAssetByIdAsync(AssetId assetId) {
+ log.trace("Executing findAssetById [{}]", assetId);
+ validateId(assetId, "Incorrect assetId " + assetId);
+ ListenableFuture<AssetEntity> assetEntity = assetDao.findByIdAsync(assetId.getId());
+ return Futures.transform(assetEntity, (Function<? super AssetEntity, ? extends Asset>) input -> getData(input));
+ }
+
+ @Override
+ public Optional<Asset> findAssetByTenantIdAndName(TenantId tenantId, String name) {
+ log.trace("Executing findAssetByTenantIdAndName [{}][{}]", tenantId, name);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Optional<AssetEntity> assetEntityOpt = assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name);
+ if (assetEntityOpt.isPresent()) {
+ return Optional.of(getData(assetEntityOpt.get()));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public Asset saveAsset(Asset asset) {
+ log.trace("Executing saveAsset [{}]", asset);
+ assetValidator.validate(asset);
+ return getData(assetDao.save(asset));
+ }
+
+ @Override
+ public Asset assignAssetToCustomer(AssetId assetId, CustomerId customerId) {
+ Asset asset = findAssetById(assetId);
+ asset.setCustomerId(customerId);
+ return saveAsset(asset);
+ }
+
+ @Override
+ public Asset unassignAssetFromCustomer(AssetId assetId) {
+ Asset asset = findAssetById(assetId);
+ asset.setCustomerId(null);
+ return saveAsset(asset);
+ }
+
+ @Override
+ public void deleteAsset(AssetId assetId) {
+ log.trace("Executing deleteAsset [{}]", assetId);
+ validateId(assetId, "Incorrect assetId " + assetId);
+ deleteEntityRelations(assetId);
+ assetDao.removeById(assetId.getId());
+ }
+
+ @Override
+ public TextPageData<Asset> 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<AssetEntity> assetEntities = assetDao.findAssetsByTenantId(tenantId.getId(), pageLink);
+ List<Asset> assets = convertDataList(assetEntities);
+ return new TextPageData<Asset>(assets, pageLink);
+ }
+
+ @Override
+ public ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds) {
+ log.trace("Executing findAssetsByTenantIdAndIdsAsync, tenantId [{}], assetIds [{}]", tenantId, assetIds);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validateIds(assetIds, "Incorrect assetIds " + assetIds);
+ ListenableFuture<List<AssetEntity>> assetEntities = assetDao.findAssetsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(assetIds));
+ return Futures.transform(assetEntities, (Function<List<AssetEntity>, List<Asset>>) input -> convertDataList(input));
+ }
+
+ @Override
+ public void deleteAssetsByTenantId(TenantId tenantId) {
+ log.trace("Executing deleteAssetsByTenantId, tenantId [{}]", tenantId);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ tenantAssetsRemover.removeEntitites(tenantId);
+ }
+
+ @Override
+ public TextPageData<Asset> 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<AssetEntity> assetEntities = assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
+ List<Asset> assets = convertDataList(assetEntities);
+ return new TextPageData<Asset>(assets, pageLink);
+ }
+
+ @Override
+ public ListenableFuture<List<Asset>> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<AssetId> assetIds) {
+ log.trace("Executing findAssetsByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], assetIds [{}]", tenantId, customerId, assetIds);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validateId(customerId, "Incorrect customerId " + customerId);
+ validateIds(assetIds, "Incorrect assetIds " + assetIds);
+ ListenableFuture<List<AssetEntity>> assetEntities = assetDao.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId.getId(),
+ customerId.getId(), toUUIDs(assetIds));
+ return Futures.transform(assetEntities, (Function<List<AssetEntity>, List<Asset>>) input -> convertDataList(input));
+ }
+
+ @Override
+ public void unassignCustomerAssets(TenantId tenantId, CustomerId customerId) {
+ log.trace("Executing unassignCustomerAssets, tenantId [{}], customerId [{}]", tenantId, customerId);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validateId(customerId, "Incorrect customerId " + customerId);
+ new CustomerAssetsUnassigner(tenantId).removeEntitites(customerId);
+ }
+
+ @Override
+ public ListenableFuture<List<Asset>> findAssetsByQuery(AssetSearchQuery query) {
+ ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
+ ListenableFuture<List<Asset>> assets = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Asset>>) relations1 -> {
+ EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
+ List<ListenableFuture<Asset>> 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<Asset>, List<Asset>>() {
+ @Nullable
+ @Override
+ public List<Asset> apply(@Nullable List<Asset> assetList) {
+ return assetList.stream().filter(asset -> query.getAssetTypes().contains(asset.getType())).collect(Collectors.toList());
+ }
+ });
+
+ return assets;
+ }
+
+ private DataValidator<Asset> assetValidator =
+ new DataValidator<Asset>() {
+
+ @Override
+ protected void validateCreate(Asset asset) {
+ assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
+ d -> {
+ throw new DataValidationException("Asset with such name already exists!");
+ }
+ );
+ }
+
+ @Override
+ protected void validateUpdate(Asset asset) {
+ assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
+ d -> {
+ if (!d.getId().equals(asset.getUuidId())) {
+ throw new DataValidationException("Asset with such name already exists!");
+ }
+ }
+ );
+ }
+
+ @Override
+ protected void validateDataImpl(Asset asset) {
+ if (StringUtils.isEmpty(asset.getName())) {
+ throw new DataValidationException("Asset name should be specified!");
+ }
+ if (asset.getTenantId() == null) {
+ throw new DataValidationException("Asset should be assigned to tenant!");
+ } else {
+ TenantEntity tenant = tenantDao.findById(asset.getTenantId().getId());
+ if (tenant == null) {
+ throw new DataValidationException("Asset is referencing to non-existent tenant!");
+ }
+ }
+ if (asset.getCustomerId() == null) {
+ asset.setCustomerId(new CustomerId(NULL_UUID));
+ } else if (!asset.getCustomerId().getId().equals(NULL_UUID)) {
+ CustomerEntity customer = customerDao.findById(asset.getCustomerId().getId());
+ if (customer == null) {
+ throw new DataValidationException("Can't assign asset to non-existent customer!");
+ }
+ if (!customer.getTenantId().equals(asset.getTenantId().getId())) {
+ throw new DataValidationException("Can't assign asset to customer from different tenant!");
+ }
+ }
+ }
+ };
+
+ private PaginatedRemover<TenantId, AssetEntity> tenantAssetsRemover =
+ new PaginatedRemover<TenantId, AssetEntity>() {
+
+ @Override
+ protected List<AssetEntity> findEntities(TenantId id, TextPageLink pageLink) {
+ return assetDao.findAssetsByTenantId(id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(AssetEntity entity) {
+ deleteAsset(new AssetId(entity.getId()));
+ }
+ };
+
+ class CustomerAssetsUnassigner extends PaginatedRemover<CustomerId, AssetEntity> {
+
+ private TenantId tenantId;
+
+ CustomerAssetsUnassigner(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ @Override
+ protected List<AssetEntity> findEntities(CustomerId id, TextPageLink pageLink) {
+ return assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(AssetEntity entity) {
+ unassignAssetFromCustomer(new AssetId(entity.getId()));
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
index 01f4f99..e8b42c2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.customer;
+import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -23,16 +24,18 @@ import org.thingsboard.server.common.data.page.TextPageLink;
public interface CustomerService {
- public Customer findCustomerById(CustomerId customerId);
+ Customer findCustomerById(CustomerId customerId);
+
+ ListenableFuture<Customer> findCustomerByIdAsync(CustomerId customerId);
- public Customer saveCustomer(Customer customer);
+ Customer saveCustomer(Customer customer);
- public void deleteCustomer(CustomerId customerId);
+ void deleteCustomer(CustomerId customerId);
- public Customer findOrCreatePublicCustomer(TenantId tenantId);
-
- public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink);
+ Customer findOrCreatePublicCustomer(TenantId tenantId);
- public void deleteCustomersByTenantId(TenantId tenantId);
+ TextPageData<Customer> 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 a80c6e8..e9f6836 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.customer;
import static org.thingsboard.server.dao.DaoUtil.convertDataList;
import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.service.Validator.validateId;
import java.io.IOException;
import java.util.List;
@@ -24,31 +25,35 @@ import java.util.Optional;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.AssetEntity;
import org.thingsboard.server.dao.model.CustomerEntity;
import org.thingsboard.server.dao.model.TenantEntity;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
import org.thingsboard.server.dao.user.UserService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.service.Validator;
@Service
@Slf4j
-public class CustomerServiceImpl implements CustomerService {
+public class CustomerServiceImpl extends BaseEntityService implements CustomerService {
private static final String PUBLIC_CUSTOMER_TITLE = "Public";
@@ -76,6 +81,14 @@ public class CustomerServiceImpl implements CustomerService {
}
@Override
+ public ListenableFuture<Customer> findCustomerByIdAsync(CustomerId customerId) {
+ log.trace("Executing findCustomerByIdAsync [{}]", customerId);
+ validateId(customerId, "Incorrect customerId " + customerId);
+ ListenableFuture<CustomerEntity> customerEntity = customerDao.findByIdAsync(customerId.getId());
+ return Futures.transform(customerEntity, (Function<? super CustomerEntity, ? extends Customer>) input -> getData(input));
+ }
+
+ @Override
public Customer saveCustomer(Customer customer) {
log.trace("Executing saveCustomer [{}]", customer);
customerValidator.validate(customer);
@@ -93,7 +106,8 @@ public class CustomerServiceImpl implements CustomerService {
}
dashboardService.unassignCustomerDashboards(customer.getTenantId(), customerId);
deviceService.unassignCustomerDevices(customer.getTenantId(), customerId);
- userService.deleteCustomerUsers(customer.getTenantId(), customerId);
+ userService.deleteCustomerUsers(customer.getTenantId(), customerId);
+ deleteEntityRelations(customerId);
customerDao.removeById(customerId.getId());
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
index 8c86064..b0ebbfd 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
@@ -26,7 +26,9 @@ import org.thingsboard.server.common.data.page.TextPageLink;
public interface DashboardService {
public Dashboard findDashboardById(DashboardId dashboardId);
-
+
+ public DashboardInfo findDashboardInfoById(DashboardId dashboardId);
+
public Dashboard saveDashboard(Dashboard dashboard);
public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
index 2e0abfb..cf554f3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
@@ -30,20 +30,19 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.*;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.service.Validator;
@Service
@Slf4j
-public class DashboardServiceImpl implements DashboardService {
+public class DashboardServiceImpl extends BaseEntityService implements DashboardService {
@Autowired
private DashboardDao dashboardDao;
@@ -66,6 +65,14 @@ public class DashboardServiceImpl implements DashboardService {
}
@Override
+ public DashboardInfo findDashboardInfoById(DashboardId dashboardId) {
+ log.trace("Executing findDashboardInfoById [{}]", dashboardId);
+ Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+ DashboardInfoEntity dashboardInfoEntity = dashboardInfoDao.findById(dashboardId.getId());
+ return getData(dashboardInfoEntity);
+ }
+
+ @Override
public Dashboard saveDashboard(Dashboard dashboard) {
log.trace("Executing saveDashboard [{}]", dashboard);
dashboardValidator.validate(dashboard);
@@ -91,6 +98,7 @@ public class DashboardServiceImpl implements DashboardService {
public void deleteDashboard(DashboardId dashboardId) {
log.trace("Executing deleteDashboard [{}]", dashboardId);
Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+ deleteEntityRelations(dashboardId);
dashboardDao.removeById(dashboardId.getId());
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java
new file mode 100644
index 0000000..eb9d9de
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceSearchQuery.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntityTypeFilter;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+public class DeviceSearchQuery {
+
+ private RelationsSearchParameters parameters;
+ @Nullable
+ private String relationType;
+ @Nullable
+ private List<String> deviceTypes;
+
+ public EntityRelationsQuery toEntitySearchQuery() {
+ EntityRelationsQuery query = new EntityRelationsQuery();
+ query.setParameters(parameters);
+ query.setFilters(
+ Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
+ Collections.singletonList(EntityType.DEVICE))));
+ return query;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
index 35d3496..4715435 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
@@ -53,4 +53,7 @@ public interface DeviceService {
ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
+
+ ListenableFuture<List<Device>> findDevicesByQuery(DeviceSearchQuery query);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
index 3d1ce31..ab6fa4e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao.device;
import com.google.common.base.Function;
+import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -24,36 +25,40 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.CustomerEntity;
import org.thingsboard.server.dao.model.DeviceEntity;
import org.thingsboard.server.dao.model.TenantEntity;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
+import javax.annotation.Nullable;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.stream.Collectors;
-import static org.thingsboard.server.dao.DaoUtil.convertDataList;
-import static org.thingsboard.server.dao.DaoUtil.getData;
-import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
+import static org.thingsboard.server.dao.DaoUtil.*;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
-import static org.thingsboard.server.dao.service.Validator.validateId;
-import static org.thingsboard.server.dao.service.Validator.validateIds;
-import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.*;
@Service
@Slf4j
-public class DeviceServiceImpl implements DeviceService {
+public class DeviceServiceImpl extends BaseEntityService implements DeviceService {
@Autowired
private DeviceDao deviceDao;
@@ -132,6 +137,7 @@ public class DeviceServiceImpl implements DeviceService {
if (deviceCredentials != null) {
deviceCredentialsService.deleteDeviceCredentials(deviceCredentials);
}
+ deleteEntityRelations(deviceId);
deviceDao.removeById(deviceId.getId());
}
@@ -192,6 +198,32 @@ public class DeviceServiceImpl implements DeviceService {
new CustomerDevicesUnassigner(tenantId).removeEntitites(customerId);
}
+ @Override
+ public ListenableFuture<List<Device>> findDevicesByQuery(DeviceSearchQuery query) {
+ ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
+ ListenableFuture<List<Device>> devices = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Device>>) relations1 -> {
+ EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
+ List<ListenableFuture<Device>> futures = new ArrayList<>();
+ for (EntityRelation relation : relations1) {
+ EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
+ if (entityId.getEntityType() == EntityType.DEVICE) {
+ futures.add(findDeviceByIdAsync(new DeviceId(entityId.getId())));
+ }
+ }
+ return Futures.successfulAsList(futures);
+ });
+
+ devices = Futures.transform(devices, new Function<List<Device>, List<Device>>() {
+ @Nullable
+ @Override
+ public List<Device> apply(@Nullable List<Device> deviceList) {
+ return deviceList.stream().filter(device -> query.getDeviceTypes().contains(device.getType())).collect(Collectors.toList());
+ }
+ });
+
+ return devices;
+ }
+
private DataValidator<Device> deviceValidator =
new DataValidator<Device>() {
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 0000000..3c1c528
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.entity;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.dao.relation.RelationService;
+
+/**
+ * Created by ashvayka on 04.05.17.
+ */
+@Slf4j
+public class BaseEntityService {
+
+ @Autowired
+ protected RelationService relationService;
+
+ protected void deleteEntityRelations(EntityId entityId) {
+ log.trace("Executing deleteEntityRelations [{}]", entityId);
+ relationService.deleteEntityRelations(entityId);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java
new file mode 100644
index 0000000..0444d11
--- /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<Asset> {
+
+ @Transient
+ private static final long serialVersionUID = -1265181166886910152L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @PartitionKey(value = 1)
+ @Column(name = ASSET_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @PartitionKey(value = 2)
+ @Column(name = ASSET_CUSTOMER_ID_PROPERTY)
+ private UUID customerId;
+
+ @Column(name = ASSET_NAME_PROPERTY)
+ private String name;
+
+ @Column(name = ASSET_TYPE_PROPERTY)
+ private String type;
+
+ @Column(name = SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ @Column(name = ASSET_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
+ private JsonNode additionalInfo;
+
+ public AssetEntity() {
+ super();
+ }
+
+ public AssetEntity(Asset asset) {
+ if (asset.getId() != null) {
+ this.id = asset.getId().getId();
+ }
+ if (asset.getTenantId() != null) {
+ this.tenantId = asset.getTenantId().getId();
+ }
+ if (asset.getCustomerId() != null) {
+ this.customerId = asset.getCustomerId().getId();
+ }
+ this.name = asset.getName();
+ this.type = asset.getType();
+ this.additionalInfo = asset.getAdditionalInfo();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public UUID getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(UUID customerId) {
+ this.customerId = customerId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return name;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ AssetEntity other = (AssetEntity) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (customerId == null) {
+ if (other.customerId != null)
+ return false;
+ } else if (!customerId.equals(other.customerId))
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (type == null) {
+ if (other.type != null)
+ return false;
+ } else if (!type.equals(other.type))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("AssetEntity [id=");
+ builder.append(id);
+ builder.append(", tenantId=");
+ builder.append(tenantId);
+ builder.append(", customerId=");
+ builder.append(customerId);
+ builder.append(", name=");
+ builder.append(name);
+ builder.append(", type=");
+ builder.append(type);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public Asset toData() {
+ Asset asset = new Asset(new AssetId(id));
+ asset.setCreatedTime(UUIDs.unixTimestamp(id));
+ if (tenantId != null) {
+ asset.setTenantId(new TenantId(tenantId));
+ }
+ if (customerId != null) {
+ asset.setCustomerId(new CustomerId(customerId));
+ }
+ asset.setName(name);
+ asset.setType(type);
+ asset.setAdditionalInfo(additionalInfo);
+ return asset;
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
index 740bf2a..69ed92c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
@@ -51,7 +51,10 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
@Column(name = DEVICE_NAME_PROPERTY)
private String name;
-
+
+ @Column(name = DEVICE_TYPE_PROPERTY)
+ private String type;
+
@Column(name = SEARCH_TEXT_PROPERTY)
private String searchText;
@@ -73,6 +76,7 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
this.customerId = device.getCustomerId().getId();
}
this.name = device.getName();
+ this.type = device.getType();
this.additionalInfo = device.getAdditionalInfo();
}
@@ -108,6 +112,14 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
this.name = name;
}
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
public JsonNode getAdditionalInfo() {
return additionalInfo;
}
@@ -138,6 +150,7 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
return result;
}
@@ -171,6 +184,11 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
return false;
} else if (!name.equals(other.name))
return false;
+ if (type == null) {
+ if (other.type != null)
+ return false;
+ } else if (!type.equals(other.type))
+ return false;
if (tenantId == null) {
if (other.tenantId != null)
return false;
@@ -190,6 +208,8 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
builder.append(customerId);
builder.append(", name=");
builder.append(name);
+ builder.append(", type=");
+ builder.append(type);
builder.append(", additionalInfo=");
builder.append(additionalInfo);
builder.append("]");
@@ -207,6 +227,7 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
device.setCustomerId(new CustomerId(customerId));
}
device.setName(name);
+ device.setType(type);
device.setAdditionalInfo(additionalInfo);
return device;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java
index fc22a8d..7892814 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java
@@ -35,7 +35,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
*/
@Data
@NoArgsConstructor
-@Table(name = DEVICE_COLUMN_FAMILY_NAME)
+@Table(name = EVENT_COLUMN_FAMILY_NAME)
public class EventEntity implements BaseEntity<Event> {
@Transient
@@ -98,23 +98,7 @@ public class EventEntity implements BaseEntity<Event> {
Event event = new Event(new EventId(id));
event.setCreatedTime(UUIDs.unixTimestamp(id));
event.setTenantId(new TenantId(tenantId));
- switch (entityType) {
- case TENANT:
- event.setEntityId(new TenantId(entityId));
- break;
- case DEVICE:
- event.setEntityId(new DeviceId(entityId));
- break;
- case CUSTOMER:
- event.setEntityId(new CustomerId(entityId));
- break;
- case RULE:
- event.setEntityId(new RuleId(entityId));
- break;
- case PLUGIN:
- event.setEntityId(new PluginId(entityId));
- break;
- }
+ event.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
event.setBody(body);
event.setType(eventType);
event.setUid(eventUId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 9c9c66b..9f78e27 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -120,12 +120,39 @@ public class ModelConstants {
public static final String DEVICE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
public static final String DEVICE_NAME_PROPERTY = "name";
+ public static final String DEVICE_TYPE_PROPERTY = "type";
public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text";
public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
+ /**
+ * Cassandra asset constants.
+ */
+ public static final String ASSET_COLUMN_FAMILY_NAME = "asset";
+ public static final String ASSET_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String ASSET_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
+ public static final String ASSET_NAME_PROPERTY = "name";
+ public static final String ASSET_TYPE_PROPERTY = "type";
+ public static final String ASSET_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
+
+ public static final String ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_and_search_text";
+ public static final String ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_customer_and_search_text";
+ public static final String ASSET_BY_TENANT_AND_NAME_VIEW_NAME = "asset_by_tenant_and_name";
+
+ /**
+ * Cassandra entity relation constants.
+ */
+ public static final String RELATION_COLUMN_FAMILY_NAME = "relation";
+ public static final String RELATION_FROM_ID_PROPERTY = "from_id";
+ public static final String RELATION_FROM_TYPE_PROPERTY = "from_type";
+ public static final String RELATION_TO_ID_PROPERTY = "to_id";
+ public static final String RELATION_TO_TYPE_PROPERTY = "to_type";
+ public static final String RELATION_TYPE_PROPERTY = "relation_type";
+
+ public static final String RELATION_REVERSE_VIEW_NAME = "reverse_relation";
+
/**
* Cassandra device_credentials constants.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
index 99560d2..5e0eb39 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
@@ -15,13 +15,14 @@
*/
package org.thingsboard.server.dao.plugin;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
@@ -29,7 +30,9 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.dao.component.ComponentDescriptorService;
+import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DatabaseException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -48,10 +51,11 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.dao.DaoUtil.convertDataList;
import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@Slf4j
-public class BasePluginService implements PluginService {
+public class BasePluginService extends BaseEntityService implements PluginService {
//TODO: move to a better place.
public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
@@ -109,6 +113,13 @@ public class BasePluginService implements PluginService {
}
@Override
+ public ListenableFuture<PluginMetaData> findPluginByIdAsync(PluginId pluginId) {
+ validateId(pluginId, "Incorrect plugin id for search plugin request.");
+ ListenableFuture<PluginMetaDataEntity> pluginEntity = pluginDao.findByIdAsync(pluginId.getId());
+ return Futures.transform(pluginEntity, (com.google.common.base.Function<? super PluginMetaDataEntity, ? extends PluginMetaData>) input -> getData(input));
+ }
+
+ @Override
public PluginMetaData findPluginByApiToken(String apiToken) {
Validator.validateString(apiToken, "Incorrect plugin apiToken for search request.");
return getData(pluginDao.findByApiToken(apiToken));
@@ -205,6 +216,7 @@ public class BasePluginService implements PluginService {
@Override
public void deletePluginById(PluginId pluginId) {
Validator.validateId(pluginId, "Incorrect plugin id for delete request.");
+ deleteEntityRelations(pluginId);
checkRulesAndDelete(pluginId.getId());
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java
index f8173ff..371a46e 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<PluginMetaData> findPluginByIdAsync(PluginId pluginId);
+
PluginMetaData findPluginByApiToken(String apiToken);
TextPageData<PluginMetaData> 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 0000000..5fd6632
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
@@ -0,0 +1,279 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.relation;
+
+import com.datastax.driver.core.*;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.AbstractAsyncDao;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 25.04.17.
+ */
+@Component
+@Slf4j
+public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
+
+ private static final String SELECT_COLUMNS = "SELECT " +
+ ModelConstants.RELATION_FROM_ID_PROPERTY + "," +
+ ModelConstants.RELATION_FROM_TYPE_PROPERTY + "," +
+ ModelConstants.RELATION_TO_ID_PROPERTY + "," +
+ ModelConstants.RELATION_TO_TYPE_PROPERTY + "," +
+ ModelConstants.RELATION_TYPE_PROPERTY + "," +
+ ModelConstants.ADDITIONAL_INFO_PROPERTY;
+ public static final String FROM = " FROM ";
+ public static final String WHERE = " WHERE ";
+ public static final String AND = " AND ";
+
+ private PreparedStatement saveStmt;
+ private PreparedStatement findAllByFromStmt;
+ private PreparedStatement findAllByFromAndTypeStmt;
+ private PreparedStatement findAllByToStmt;
+ private PreparedStatement findAllByToAndTypeStmt;
+ private PreparedStatement checkRelationStmt;
+ private PreparedStatement deleteStmt;
+ private PreparedStatement deleteAllByEntityStmt;
+
+ @PostConstruct
+ public void init() {
+ super.startExecutor();
+ }
+
+ @Override
+ public ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from) {
+ BoundStatement stmt = getFindAllByFromStmt().bind().setUUID(0, from.getId()).setString(1, from.getEntityType().name());
+ return executeAsyncRead(from, stmt);
+ }
+
+ @Override
+ public ListenableFuture<List<EntityRelation>> 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<List<EntityRelation>> findAllByTo(EntityId to) {
+ BoundStatement stmt = getFindAllByToStmt().bind().setUUID(0, to.getId()).setString(1, to.getEntityType().name());
+ return executeAsyncRead(to, stmt);
+ }
+
+ @Override
+ public ListenableFuture<List<EntityRelation>> 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<Boolean> 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<Boolean> 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<Boolean> deleteRelation(EntityRelation relation) {
+ return deleteRelation(relation.getFrom(), relation.getTo(), relation.getType());
+ }
+
+ @Override
+ public ListenableFuture<Boolean> 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<Boolean> 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<List<EntityRelation>> executeAsyncRead(EntityId from, BoundStatement stmt) {
+ log.debug("Generated query [{}] for entity {}", stmt, from);
+ return getFuture(executeAsyncRead(stmt), rs -> {
+ List<Row> rows = rs.all();
+ List<EntityRelation> entries = new ArrayList<>(rows.size());
+ if (!rows.isEmpty()) {
+ rows.forEach(row -> {
+ entries.add(getEntityRelation(row));
+ });
+ }
+ return entries;
+ });
+ }
+
+ private ListenableFuture<Boolean> getBooleanListenableFuture(ResultSetFuture rsFuture) {
+ return getFuture(rsFuture, rs -> rs != null ? rs.wasApplied() : false);
+ }
+
+ private <T> ListenableFuture<T> getFuture(ResultSetFuture future, java.util.function.Function<ResultSet, T> transformer) {
+ return Futures.transform(future, new Function<ResultSet, T>() {
+ @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 0000000..1b572c6
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -0,0 +1,257 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.relation;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.exception.DataValidationException;
+
+import javax.annotation.Nullable;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by ashvayka on 28.04.17.
+ */
+@Service
+@Slf4j
+public class BaseRelationService implements RelationService {
+
+ @Autowired
+ private RelationDao relationDao;
+
+ @Override
+ public ListenableFuture<Boolean> 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<Boolean> saveRelation(EntityRelation relation) {
+ log.trace("Executing saveRelation [{}]", relation);
+ validate(relation);
+ return relationDao.saveRelation(relation);
+ }
+
+ @Override
+ public ListenableFuture<Boolean> deleteRelation(EntityRelation relation) {
+ log.trace("Executing deleteRelation [{}]", relation);
+ validate(relation);
+ return relationDao.deleteRelation(relation);
+ }
+
+ @Override
+ public ListenableFuture<Boolean> 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<Boolean> deleteEntityRelations(EntityId entity) {
+ log.trace("Executing deleteEntityRelations [{}]", entity);
+ validate(entity);
+ ListenableFuture<List<EntityRelation>> inboundRelations = relationDao.findAllByTo(entity);
+ ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, new AsyncFunction<List<EntityRelation>, List<Boolean>>() {
+ @Override
+ public ListenableFuture<List<Boolean>> apply(List<EntityRelation> relations) throws Exception {
+ List<ListenableFuture<Boolean>> results = new ArrayList<>();
+ for (EntityRelation relation : relations) {
+ results.add(relationDao.deleteRelation(relation));
+ }
+ return Futures.allAsList(results);
+ }
+ });
+
+ ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
+
+ ListenableFuture<Boolean> outboundFuture = relationDao.deleteOutboundRelations(entity);
+
+ return Futures.transform(Futures.allAsList(Arrays.asList(inboundFuture, outboundFuture)), getListToBooleanFunction());
+ }
+
+ @Override
+ public ListenableFuture<List<EntityRelation>> findByFrom(EntityId from) {
+ log.trace("Executing findByFrom [{}]", from);
+ validate(from);
+ return relationDao.findAllByFrom(from);
+ }
+
+ @Override
+ public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType) {
+ log.trace("Executing findByFromAndType [{}][{}]", from, relationType);
+ validate(from);
+ validateType(relationType);
+ return relationDao.findAllByFromAndType(from, relationType);
+ }
+
+ @Override
+ public ListenableFuture<List<EntityRelation>> findByTo(EntityId to) {
+ log.trace("Executing findByTo [{}]", to);
+ validate(to);
+ return relationDao.findAllByTo(to);
+ }
+
+ @Override
+ public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType) {
+ log.trace("Executing findByToAndType [{}][{}]", to, relationType);
+ validate(to);
+ validateType(relationType);
+ return relationDao.findAllByToAndType(to, relationType);
+ }
+
+ @Override
+ public ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query) {
+ log.trace("Executing findByQuery [{}][{}]", query);
+ RelationsSearchParameters params = query.getParameters();
+ final List<EntityTypeFilter> filters = query.getFilters();
+ if (filters == null || filters.isEmpty()) {
+ log.warn("Failed to query relations. Filters are not set [{}]", query);
+ throw new RuntimeException("Filters are not set!");
+ }
+
+ int maxLvl = params.getMaxLevel() > 0 ? params.getMaxLevel() : Integer.MAX_VALUE;
+
+ try {
+ ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(params.getEntityId(), params.getDirection(), maxLvl, new ConcurrentHashMap<>());
+ return Futures.transform(relationSet, (Function<Set<EntityRelation>, List<EntityRelation>>) input -> {
+ List<EntityRelation> relations = new ArrayList<>();
+ for (EntityRelation relation : input) {
+ for (EntityTypeFilter filter : filters) {
+ if (match(filter, relation, params.getDirection())) {
+ relations.add(relation);
+ break;
+ }
+ }
+ }
+ return relations;
+ });
+ } catch (Exception e) {
+ log.warn("Failed to query relations: [{}]", query, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected void validate(EntityRelation relation) {
+ if (relation == null) {
+ throw new DataValidationException("Relation type should be specified!");
+ }
+ validate(relation.getFrom(), relation.getTo(), relation.getType());
+ }
+
+ protected void validate(EntityId from, EntityId to, String type) {
+ validateType(type);
+ if (from == null) {
+ throw new DataValidationException("Relation should contain from entity!");
+ }
+ if (to == null) {
+ throw new DataValidationException("Relation should contain to entity!");
+ }
+ }
+
+ private void validateType(String type) {
+ if (StringUtils.isEmpty(type)) {
+ throw new DataValidationException("Relation type should be specified!");
+ }
+ }
+
+ protected void validate(EntityId entity) {
+ if (entity == null) {
+ throw new DataValidationException("Entity should be specified!");
+ }
+ }
+
+ private Function<List<Boolean>, Boolean> getListToBooleanFunction() {
+ return new Function<List<Boolean>, Boolean>() {
+ @Nullable
+ @Override
+ public Boolean apply(@Nullable List<Boolean> 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<Set<EntityRelation>> findRelationsRecursively(final EntityId rootId, final EntitySearchDirection direction, int lvl, final ConcurrentHashMap<EntityId, Boolean> uniqueMap) throws Exception {
+ if (lvl == 0) {
+ return Futures.immediateFuture(Collections.emptySet());
+ }
+ lvl--;
+ //TODO: try to remove this blocking operation
+ Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get());
+ Set<EntityId> 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<ListenableFuture<Set<EntityRelation>>> futures = new ArrayList<>();
+ for (EntityId entityId : childrenIds) {
+ futures.add(findRelationsRecursively(entityId, direction, lvl, uniqueMap));
+ }
+ //TODO: try to remove this blocking operation
+ List<Set<EntityRelation>> relations = Futures.successfulAsList(futures).get();
+ relations.forEach(r -> r.forEach(d -> children.add(d)));
+ return Futures.immediateFuture(children);
+ }
+
+ private ListenableFuture<List<EntityRelation>> findRelations(final EntityId rootId, final EntitySearchDirection direction) {
+ ListenableFuture<List<EntityRelation>> 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 0000000..b19e9e5
--- /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<EntityTypeFilter> 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 0000000..64715b1
--- /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 0000000..9618ecf
--- /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<EntityType> 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 0000000..df47259
--- /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<List<EntityRelation>> findAllByFrom(EntityId from);
+
+ ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType);
+
+ ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to);
+
+ ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType);
+
+ ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType);
+
+ ListenableFuture<Boolean> saveRelation(EntityRelation relation);
+
+ ListenableFuture<Boolean> deleteRelation(EntityRelation relation);
+
+ ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType);
+
+ ListenableFuture<Boolean> 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 0000000..f4f3a37
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.relation;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+
+import java.util.List;
+
+/**
+ * Created by ashvayka on 27.04.17.
+ */
+public interface RelationService {
+
+ ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType);
+
+ ListenableFuture<Boolean> saveRelation(EntityRelation relation);
+
+ ListenableFuture<Boolean> deleteRelation(EntityRelation relation);
+
+ ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType);
+
+ ListenableFuture<Boolean> deleteEntityRelations(EntityId entity);
+
+ ListenableFuture<List<EntityRelation>> findByFrom(EntityId from);
+
+ ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType);
+
+ ListenableFuture<List<EntityRelation>> findByTo(EntityId to);
+
+ ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType);
+
+ ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationsSearchParameters.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationsSearchParameters.java
new file mode 100644
index 0000000..65920d7
--- /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 e57c9f9..fb5e9ce 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
@@ -17,10 +17,13 @@ package org.thingsboard.server.dao.rule;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@@ -31,9 +34,11 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.dao.component.ComponentDescriptorService;
+import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DatabaseException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.AssetEntity;
import org.thingsboard.server.dao.model.RuleMetaDataEntity;
import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.service.DataValidator;
@@ -53,7 +58,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@Slf4j
-public class BaseRuleService implements RuleService {
+public class BaseRuleService extends BaseEntityService implements RuleService {
private final TenantId systemTenantId = new TenantId(NULL_UUID);
@@ -167,6 +172,13 @@ public class BaseRuleService implements RuleService {
}
@Override
+ public ListenableFuture<RuleMetaData> findRuleByIdAsync(RuleId ruleId) {
+ validateId(ruleId, "Incorrect rule id for search rule request.");
+ ListenableFuture<RuleMetaDataEntity> ruleEntity = ruleDao.findByIdAsync(ruleId.getId());
+ return Futures.transform(ruleEntity, (com.google.common.base.Function<? super RuleMetaDataEntity, ? extends RuleMetaData>) input -> getData(input));
+ }
+
+ @Override
public List<RuleMetaData> findPluginRules(String pluginToken) {
List<RuleMetaDataEntity> ruleEntities = ruleDao.findRulesByPlugin(pluginToken);
return convertDataList(ruleEntities);
@@ -235,6 +247,7 @@ public class BaseRuleService implements RuleService {
@Override
public void deleteRuleById(RuleId ruleId) {
validateId(ruleId, "Incorrect rule id for delete rule request.");
+ deleteEntityRelations(ruleId);
ruleDao.deleteById(ruleId);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java
index 98ff8c0..23f4a01 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<RuleMetaData> findRuleByIdAsync(RuleId ruleId);
+
List<RuleMetaData> findPluginRules(String pluginToken);
TextPageData<RuleMetaData> 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 70e9860..bb4912e 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;
@@ -25,6 +26,19 @@ import java.util.UUID;
public class Validator {
/**
+ * This method validate <code>EntityId</code> entity id. If entity id is invalid than throw
+ * <code>IncorrectParameterException</code> 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 <code>String</code> string. If string is invalid than throw
* <code>IncorrectParameterException</code> exception
*
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
index 7e56e19..53a4ef9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.tenant;
+import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@@ -22,16 +23,16 @@ import org.thingsboard.server.common.data.page.TextPageLink;
public interface TenantService {
- public Tenant findTenantById(TenantId tenantId);
-
- public Tenant saveTenant(Tenant tenant);
+ Tenant findTenantById(TenantId tenantId);
+
+ ListenableFuture<Tenant> findTenantByIdAsync(TenantId customerId);
- public void deleteTenant(TenantId tenantId);
+ Tenant saveTenant(Tenant tenant);
- public TextPageData<Tenant> findTenants(TextPageLink pageLink);
+ void deleteTenant(TenantId tenantId);
- //public TextPageData<Tenant> findTenantsByTitle(String title, PageLink pageLink);
+ TextPageData<Tenant> findTenants(TextPageLink pageLink);
- public void deleteTenants();
+ void deleteTenants();
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
index 9deb828..bcc31a8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
@@ -17,11 +17,16 @@ package org.thingsboard.server.dao.tenant;
import static org.thingsboard.server.dao.DaoUtil.convertDataList;
import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.service.Validator.validateId;
import java.util.List;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@@ -29,15 +34,15 @@ import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.CustomerEntity;
import org.thingsboard.server.dao.model.TenantEntity;
import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.rule.RuleService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.user.UserService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.service.Validator;
@@ -45,19 +50,19 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
@Service
@Slf4j
-public class TenantServiceImpl implements TenantService {
-
+public class TenantServiceImpl extends BaseEntityService implements TenantService {
+
private static final String DEFAULT_TENANT_REGION = "Global";
@Autowired
private TenantDao tenantDao;
-
+
@Autowired
private UserService userService;
-
+
@Autowired
private CustomerService customerService;
-
+
@Autowired
private DeviceService deviceService;
@@ -72,7 +77,7 @@ public class TenantServiceImpl implements TenantService {
@Autowired
private PluginService pluginService;
-
+
@Override
public Tenant findTenantById(TenantId tenantId) {
log.trace("Executing findTenantById [{}]", tenantId);
@@ -82,6 +87,14 @@ public class TenantServiceImpl implements TenantService {
}
@Override
+ public ListenableFuture<Tenant> findTenantByIdAsync(TenantId tenantId) {
+ log.trace("Executing TenantIdAsync [{}]", tenantId);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ ListenableFuture<TenantEntity> tenantEntity = tenantDao.findByIdAsync(tenantId.getId());
+ return Futures.transform(tenantEntity, (Function<? super TenantEntity, ? extends Tenant>) input -> getData(input));
+ }
+
+ @Override
public Tenant saveTenant(Tenant tenant) {
log.trace("Executing saveTenant [{}]", tenant);
tenant.setRegion(DEFAULT_TENANT_REGION);
@@ -102,6 +115,7 @@ public class TenantServiceImpl implements TenantService {
ruleService.deleteRulesByTenantId(tenantId);
pluginService.deletePluginsByTenantId(tenantId);
tenantDao.removeById(tenantId.getId());
+ deleteEntityRelations(tenantId);
}
@Override
@@ -131,10 +145,10 @@ public class TenantServiceImpl implements TenantService {
}
}
};
-
+
private PaginatedRemover<String, TenantEntity> tenantsRemover =
new PaginatedRemover<String, TenantEntity>() {
-
+
@Override
protected List<TenantEntity> findEntities(String region, TextPageLink pageLink) {
return tenantDao.findTenantsByRegion(region, pageLink);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
index cbf27fa..26fe3fb 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
@@ -26,6 +26,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.dao.AbstractAsyncDao;
@@ -94,8 +95,8 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
}
@Override
- public ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, List<TsKvQuery> queries) {
- List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(entityType, entityId, query)).collect(Collectors.toList());
+ public ListenableFuture<List<TsKvEntry>> findAllAsync(EntityId entityId, List<TsKvQuery> queries) {
+ List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(entityId, query)).collect(Collectors.toList());
return Futures.transform(Futures.allAsList(futures), new Function<List<List<TsKvEntry>>, List<TsKvEntry>>() {
@Nullable
@Override
@@ -108,9 +109,9 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
}
- private ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, TsKvQuery query) {
+ private ListenableFuture<List<TsKvEntry>> findAllAsync(EntityId entityId, TsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) {
- return findAllAsyncWithLimit(entityType, entityId, query);
+ return findAllAsyncWithLimit(entityId, query);
} else {
long step = Math.max(query.getInterval(), minAggregationStepMs);
long stepTs = query.getStartTs();
@@ -119,7 +120,7 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
long startTs = stepTs;
long endTs = stepTs + step;
TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation());
- futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
+ futures.add(findAndAggregateAsync(entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
stepTs = endTs;
}
ListenableFuture<List<Optional<TsKvEntry>>> future = Futures.allAsList(futures);
@@ -133,11 +134,11 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
}
}
- private ListenableFuture<List<TsKvEntry>> findAllAsyncWithLimit(String entityType, UUID entityId, TsKvQuery query) {
+ private ListenableFuture<List<TsKvEntry>> 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<List<TsKvEntry>> resultFuture = new SimpleListenableFuture<>();
final ListenableFuture<List<Long>> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
@@ -145,13 +146,13 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() {
@Override
public void onSuccess(@Nullable List<Long> partitions) {
- TsKvQueryCursor cursor = new TsKvQueryCursor(entityType, entityId, query, partitions);
+ TsKvQueryCursor cursor = new TsKvQueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions);
findAllAsyncSequentiallyWithLimit(cursor, resultFuture);
}
@Override
public void onFailure(Throwable t) {
- log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityType, entityId, minPartition, maxPartition, t);
+ log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t);
}
}, readResultsProcessingExecutor);
@@ -187,19 +188,19 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
}
}
- private ListenableFuture<Optional<TsKvEntry>> findAndAggregateAsync(String entityType, UUID entityId, TsKvQuery query, long minPartition, long maxPartition) {
+ private ListenableFuture<Optional<TsKvEntry>> 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<List<Long>> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
ListenableFuture<List<ResultSet>> aggregationChunks = Futures.transform(partitionsListFuture,
- getFetchChunksAsyncFunction(entityType, entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor);
+ getFetchChunksAsyncFunction(entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor);
return Futures.transform(aggregationChunks, new AggregatePartitionsFunction(aggregation, key, ts), readResultsProcessingExecutor);
}
@@ -209,21 +210,21 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
.map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)).collect(Collectors.toList());
}
- private AsyncFunction<List<Long>, List<ResultSet>> getFetchChunksAsyncFunction(String entityType, UUID entityId, String key, Aggregation aggregation, long startTs, long endTs) {
+ private AsyncFunction<List<Long>, List<ResultSet>> getFetchChunksAsyncFunction(EntityId entityId, String key, Aggregation aggregation, long startTs, long endTs) {
return partitions -> {
try {
PreparedStatement proto = getFetchStmt(aggregation);
List<ResultSetFuture> futures = new ArrayList<>(partitions.size());
for (Long partition : partitions) {
- log.trace("Fetching data for partition [{}] for entityType {} and entityId {}", partition, entityType, entityId);
+ log.trace("Fetching data for partition [{}] for entityType {} and entityId {}", partition, entityId.getEntityType(), entityId.getId());
BoundStatement stmt = proto.bind();
- stmt.setString(0, entityType);
- stmt.setUUID(1, entityId);
+ stmt.setString(0, entityId.getEntityType().name());
+ stmt.setUUID(1, entityId.getId());
stmt.setString(2, key);
stmt.setLong(3, partition);
stmt.setLong(4, startTs);
stmt.setLong(5, endTs);
- log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId);
+ log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId());
futures.add(executeAsyncRead(stmt));
}
return Futures.allAsList(futures);
@@ -235,30 +236,30 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
}
@Override
- public ResultSetFuture findLatest(String entityType, UUID entityId, String key) {
+ public ResultSetFuture findLatest(EntityId entityId, String key) {
BoundStatement stmt = getFindLatestStmt().bind();
- stmt.setString(0, entityType);
- stmt.setUUID(1, entityId);
+ stmt.setString(0, entityId.getEntityType().name());
+ stmt.setUUID(1, entityId.getId());
stmt.setString(2, key);
- log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId);
+ log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId());
return executeAsyncRead(stmt);
}
@Override
- public ResultSetFuture findAllLatest(String entityType, UUID entityId) {
+ public ResultSetFuture findAllLatest(EntityId entityId) {
BoundStatement stmt = getFindAllLatestStmt().bind();
- stmt.setString(0, entityType);
- stmt.setUUID(1, entityId);
- log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId);
+ stmt.setString(0, entityId.getEntityType().name());
+ stmt.setUUID(1, entityId.getId());
+ log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId());
return executeAsyncRead(stmt);
}
@Override
- public ResultSetFuture save(String entityType, UUID entityId, long partition, TsKvEntry tsKvEntry) {
+ public ResultSetFuture save(EntityId entityId, long partition, TsKvEntry tsKvEntry) {
DataType type = tsKvEntry.getDataType();
BoundStatement stmt = getSaveStmt(type).bind()
- .setString(0, entityType)
- .setUUID(1, entityId)
+ .setString(0, entityId.getEntityType().name())
+ .setUUID(1, entityId.getId())
.setString(2, tsKvEntry.getKey())
.setLong(3, partition)
.setLong(4, tsKvEntry.getTs());
@@ -267,11 +268,11 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
}
@Override
- public ResultSetFuture saveLatest(String entityType, UUID entityId, TsKvEntry tsKvEntry) {
+ public ResultSetFuture saveLatest(EntityId entityId, TsKvEntry tsKvEntry) {
DataType type = tsKvEntry.getDataType();
BoundStatement stmt = getLatestStmt(type).bind()
- .setString(0, entityType)
- .setUUID(1, entityId)
+ .setString(0, entityId.getEntityType().name())
+ .setUUID(1, entityId.getId())
.setString(2, tsKvEntry.getKey())
.setLong(3, tsKvEntry.getTs());
addValue(tsKvEntry, stmt, 4);
@@ -279,11 +280,11 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
}
@Override
- public ResultSetFuture savePartition(String entityType, UUID entityId, long partition, String key) {
- log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityType, entityId, key);
+ public ResultSetFuture savePartition(EntityId entityId, long partition, String key) {
+ log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key);
return executeAsyncWrite(getPartitionInsertStmt().bind()
- .setString(0, entityType)
- .setUUID(1, entityId)
+ .setString(0, entityId.getEntityType().name())
+ .setUUID(1, entityId.getId())
.setLong(2, partition)
.setString(3, key));
}
@@ -339,9 +340,9 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
* Select existing partitions from the table
* <code>{@link ModelConstants#TS_KV_PARTITIONS_CF}</code> for the given entity
*/
- private ResultSetFuture fetchPartitions(String entityType, UUID entityId, String key, long minPartition, long maxPartition) {
- Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityType))
- .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId)).and(eq(ModelConstants.KEY_COLUMN, key));
+ private ResultSetFuture fetchPartitions(EntityId entityId, String key, long minPartition, long maxPartition) {
+ Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityId.getEntityType().name()))
+ .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId.getId())).and(eq(ModelConstants.KEY_COLUMN, key));
select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
return executeAsyncRead(select);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
index f27ed6e..5d722f9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
@@ -23,6 +23,7 @@ import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
@@ -59,30 +60,30 @@ public class BaseTimeseriesService implements TimeseriesService {
private TimeseriesDao timeseriesDao;
@Override
- public ListenableFuture<List<TsKvEntry>> findAll(String entityType, UUIDBased entityId, List<TsKvQuery> queries) {
- validate(entityType, entityId);
+ public ListenableFuture<List<TsKvEntry>> findAll(EntityId entityId, List<TsKvQuery> queries) {
+ validate(entityId);
queries.forEach(query -> validate(query));
- return timeseriesDao.findAllAsync(entityType, entityId.getId(), queries);
+ return timeseriesDao.findAllAsync(entityId, queries);
}
@Override
- public ListenableFuture<List<ResultSet>> findLatest(String entityType, UUIDBased entityId, Collection<String> keys) {
- validate(entityType, entityId);
+ public ListenableFuture<List<ResultSet>> findLatest(EntityId entityId, Collection<String> keys) {
+ validate(entityId);
List<ResultSetFuture> 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<List<ResultSet>> save(String entityType, UUIDBased entityId, TsKvEntry tsKvEntry) {
- validate(entityType, entityId);
+ public ListenableFuture<List<ResultSet>> save(EntityId entityId, TsKvEntry tsKvEntry) {
+ validate(entityId);
if (tsKvEntry == null) {
throw new IncorrectParameterException("Key value entry can't be null");
}
@@ -90,13 +91,13 @@ public class BaseTimeseriesService implements TimeseriesService {
long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs());
List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
- saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs);
+ saveAndRegisterFutures(futures, entityId, tsKvEntry, partitionTs);
return Futures.allAsList(futures);
}
@Override
- public ListenableFuture<List<ResultSet>> save(String entityType, UUIDBased entityId, List<TsKvEntry> tsKvEntries) {
- validate(entityType, entityId);
+ public ListenableFuture<List<ResultSet>> save(EntityId entityId, List<TsKvEntry> tsKvEntries) {
+ validate(entityId);
List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * INSERTS_PER_ENTRY);
for (TsKvEntry tsKvEntry : tsKvEntries) {
if (tsKvEntry == null) {
@@ -104,7 +105,7 @@ public class BaseTimeseriesService implements TimeseriesService {
}
UUID uid = entityId.getId();
long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs());
- saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs);
+ saveAndRegisterFutures(futures, entityId, tsKvEntry, partitionTs);
}
return Futures.allAsList(futures);
}
@@ -119,15 +120,14 @@ public class BaseTimeseriesService implements TimeseriesService {
return timeseriesDao.convertResultToTsKvEntryList(rs.all());
}
- private void saveAndRegisterFutures(List<ResultSetFuture> 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<ResultSetFuture> futures, EntityId entityId, TsKvEntry tsKvEntry, long partitionTs) {
+ futures.add(timeseriesDao.savePartition(entityId, partitionTs, tsKvEntry.getKey()));
+ futures.add(timeseriesDao.saveLatest(entityId, tsKvEntry));
+ futures.add(timeseriesDao.save(entityId, partitionTs, tsKvEntry));
}
- private static void validate(String entityType, UUIDBased entityId) {
- Validator.validateString(entityType, "Incorrect entityType " + entityType);
- Validator.validateId(entityId, "Incorrect entityId " + entityId);
+ private static void validate(EntityId entityId) {
+ Validator.validateEntityId(entityId, "Incorrect entityId " + entityId);
}
private static void validate(TsKvQuery query) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
index 177003d..1f9871e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.timeseries;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row;
import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
@@ -33,17 +34,17 @@ public interface TimeseriesDao {
long toPartitionTs(long ts);
- ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, List<TsKvQuery> queries);
+ ListenableFuture<List<TsKvEntry>> findAllAsync(EntityId entityId, List<TsKvQuery> 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 cd53e94..5c9c961 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
@@ -20,6 +20,7 @@ import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
@@ -33,15 +34,15 @@ import java.util.Set;
*/
public interface TimeseriesService {
- ListenableFuture<List<TsKvEntry>> findAll(String entityType, UUIDBased entityId, List<TsKvQuery> queries);
+ ListenableFuture<List<TsKvEntry>> findAll(EntityId entityId, List<TsKvQuery> queries);
- ListenableFuture<List<ResultSet>> findLatest(String entityType, UUIDBased entityId, Collection<String> keys);
+ ListenableFuture<List<ResultSet>> findLatest(EntityId entityId, Collection<String> keys);
- ResultSetFuture findAllLatest(String entityType, UUIDBased entityId);
+ ResultSetFuture findAllLatest(EntityId entityId);
- ListenableFuture<List<ResultSet>> save(String entityType, UUIDBased entityId, TsKvEntry tsKvEntry);
+ ListenableFuture<List<ResultSet>> save(EntityId entityId, TsKvEntry tsKvEntry);
- ListenableFuture<List<ResultSet>> save(String entityType, UUIDBased entityId, List<TsKvEntry> tsKvEntry);
+ ListenableFuture<List<ResultSet>> save(EntityId entityId, List<TsKvEntry> 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 9ad9102..6410111 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
@@ -35,20 +35,19 @@ import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.*;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
-public class UserServiceImpl implements UserService {
+public class UserServiceImpl extends BaseEntityService implements UserService {
@Autowired
private UserDao userDao;
@@ -169,6 +168,7 @@ public class UserServiceImpl implements UserService {
validateId(userId, "Incorrect userId " + userId);
UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByUserId(userId.getId());
userCredentialsDao.removeById(userCredentialsEntity.getId());
+ deleteEntityRelations(userId);
userDao.removeById(userId.getId());
}
dao/src/main/resources/schema.cql 51(+51 -0)
diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql
index 6e45430..c3e7cda 100644
--- a/dao/src/main/resources/schema.cql
+++ b/dao/src/main/resources/schema.cql
@@ -156,6 +156,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.device (
tenant_id timeuuid,
customer_id timeuuid,
name text,
+ type text,
search_text text,
additional_info text,
PRIMARY KEY (id, tenant_id, customer_id)
@@ -202,6 +203,56 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_credentials_by_credent
WHERE credentials_id IS NOT NULL AND id IS NOT NULL
PRIMARY KEY ( credentials_id, id );
+
+CREATE TABLE IF NOT EXISTS thingsboard.asset (
+ id timeuuid,
+ tenant_id timeuuid,
+ customer_id timeuuid,
+ name text,
+ type text,
+ search_text text,
+ additional_info text,
+ PRIMARY KEY (id, tenant_id, customer_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_name AS
+ SELECT *
+ from thingsboard.asset
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, name, id, customer_id)
+ WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.asset
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, search_text, id, customer_id)
+ WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_and_search_text AS
+ SELECT *
+ from thingsboard.asset
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( customer_id, tenant_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.relation (
+ from_id timeuuid,
+ from_type text,
+ to_id timeuuid,
+ to_type text,
+ relation_type text,
+ additional_info text,
+ PRIMARY KEY ((from_id, from_type), relation_type, to_id, to_type)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.reverse_relation AS
+SELECT *
+from thingsboard.relation
+WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
+PRIMARY KEY ((to_id, to_type), relation_type, from_id, from_type)
+WITH CLUSTERING ORDER BY ( relation_type ASC, from_id ASC, from_type ASC);
+
CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle (
id timeuuid,
tenant_id timeuuid,
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index f66e6ee..903207c 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -47,6 +47,7 @@ import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.tenant.TenantService;
@@ -111,6 +112,9 @@ public abstract class AbstractServiceTest {
protected EventService eventService;
@Autowired
+ protected RelationService relationService;
+
+ @Autowired
private ComponentDescriptorService componentDescriptorService;
class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
new file mode 100644
index 0000000..3f5c55a
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
@@ -0,0 +1,283 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
+import org.thingsboard.server.dao.relation.EntityTypeFilter;
+import org.thingsboard.server.dao.relation.RelationsSearchParameters;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class RelationServiceImplTest extends AbstractServiceTest {
+
+ @Before
+ public void before() {
+ }
+
+ @After
+ public void after() {
+ }
+
+ @Test
+ public void testSaveRelation() throws ExecutionException, InterruptedException {
+ AssetId parentId = new AssetId(UUIDs.timeBased());
+ AssetId childId = new AssetId(UUIDs.timeBased());
+
+ EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+
+ Assert.assertTrue(saveRelation(relation));
+
+ Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+
+ Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE").get());
+
+ Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE).get());
+
+ Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE").get());
+ }
+
+ @Test
+ public void testDeleteRelation() throws ExecutionException, InterruptedException {
+ AssetId parentId = new AssetId(UUIDs.timeBased());
+ AssetId childId = new AssetId(UUIDs.timeBased());
+ AssetId subChildId = new AssetId(UUIDs.timeBased());
+
+ EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+ EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE);
+
+ saveRelation(relationA);
+ saveRelation(relationB);
+
+ Assert.assertTrue(relationService.deleteRelation(relationA).get());
+
+ Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+
+ Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+
+ Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+ }
+
+ @Test
+ public void testDeleteEntityRelations() throws ExecutionException, InterruptedException {
+ AssetId parentId = new AssetId(UUIDs.timeBased());
+ AssetId childId = new AssetId(UUIDs.timeBased());
+ AssetId subChildId = new AssetId(UUIDs.timeBased());
+
+ EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+ EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE);
+
+ saveRelation(relationA);
+ saveRelation(relationB);
+
+ Assert.assertTrue(relationService.deleteEntityRelations(childId).get());
+
+ Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+
+ Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+ }
+
+ @Test
+ public void testFindFrom() throws ExecutionException, InterruptedException {
+ AssetId parentA = new AssetId(UUIDs.timeBased());
+ AssetId parentB = new AssetId(UUIDs.timeBased());
+ AssetId childA = new AssetId(UUIDs.timeBased());
+ AssetId childB = new AssetId(UUIDs.timeBased());
+
+ EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE);
+ EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE);
+
+ EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE);
+ EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE);
+
+ saveRelation(relationA1);
+ saveRelation(relationA2);
+
+ saveRelation(relationB1);
+ saveRelation(relationB2);
+
+ List<EntityRelation> 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<EntityRelation> 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<EntityRelation> 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<EntityRelation> relations = relationService.findByQuery(query).get();
+ Assert.assertEquals(2, relations.size());
+ Assert.assertTrue(relations.contains(relationAB));
+ Assert.assertTrue(relations.contains(relationBC));
+ }
+
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException {
+ EntityRelation relation = new EntityRelation();
+ relation.setTo(new AssetId(UUIDs.timeBased()));
+ relation.setType(EntityRelation.CONTAINS_TYPE);
+ Assert.assertTrue(saveRelation(relation));
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveRelationWithEmptyTo() throws ExecutionException, InterruptedException {
+ EntityRelation relation = new EntityRelation();
+ relation.setFrom(new AssetId(UUIDs.timeBased()));
+ relation.setType(EntityRelation.CONTAINS_TYPE);
+ Assert.assertTrue(saveRelation(relation));
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveRelationWithEmptyType() throws ExecutionException, InterruptedException {
+ EntityRelation relation = new EntityRelation();
+ relation.setFrom(new AssetId(UUIDs.timeBased()));
+ relation.setTo(new AssetId(UUIDs.timeBased()));
+ Assert.assertTrue(saveRelation(relation));
+ }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
index 134f471..8ced7ea 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
@@ -66,7 +66,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
saveEntries(deviceId, TS - 1);
saveEntries(deviceId, TS);
- ResultSetFuture rsFuture = tsService.findAllLatest(DataConstants.DEVICE, deviceId);
+ ResultSetFuture rsFuture = tsService.findAllLatest(deviceId);
List<TsKvEntry> tsList = tsService.convertResultSetToTsKvEntryList(rsFuture.get());
assertNotNull(tsList);
@@ -95,7 +95,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
saveEntries(deviceId, TS - 1);
saveEntries(deviceId, TS);
- List<ResultSet> rs = tsService.findLatest(DataConstants.DEVICE, deviceId, Collections.singleton(STRING_KEY)).get();
+ List<ResultSet> rs = tsService.findLatest(deviceId, Collections.singleton(STRING_KEY)).get();
Assert.assertEquals(1, rs.size());
Assert.assertEquals(toTsEntry(TS, stringKvEntry), tsService.convertResultToTsKvEntry(rs.get(0).one()));
}
@@ -114,7 +114,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
entries.add(save(deviceId, 45000, 500));
entries.add(save(deviceId, 55000, 600));
- List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+ List<TsKvEntry> list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.NONE))).get();
assertEquals(3, list.size());
assertEquals(55000, list.get(0).getTs());
@@ -126,7 +126,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(35000, list.get(2).getTs());
assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue());
- list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+ list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.AVG))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
@@ -138,7 +138,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue());
- list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+ list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.SUM))).get();
assertEquals(3, list.size());
@@ -151,7 +151,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue());
- list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+ list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.MIN))).get();
assertEquals(3, list.size());
@@ -164,7 +164,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue());
- list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+ list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.MAX))).get();
assertEquals(3, list.size());
@@ -177,7 +177,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue());
- list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+ list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.COUNT))).get();
assertEquals(3, list.size());
@@ -193,15 +193,15 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception {
TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value));
- tsService.save(DataConstants.DEVICE, deviceId, entry).get();
+ tsService.save(deviceId, entry).get();
return entry;
}
private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException {
- tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, stringKvEntry)).get();
- tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, longKvEntry)).get();
- tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, doubleKvEntry)).get();
- tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, booleanKvEntry)).get();
+ tsService.save(deviceId, toTsEntry(ts, stringKvEntry)).get();
+ tsService.save(deviceId, toTsEntry(ts, longKvEntry)).get();
+ tsService.save(deviceId, toTsEntry(ts, doubleKvEntry)).get();
+ tsService.save(deviceId, toTsEntry(ts, booleanKvEntry)).get();
}
private static TsKvEntry toTsEntry(long ts, KvEntry entry) {
docker/docker-compose.yml 4(+2 -2)
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index d19332b..97873e8 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -18,7 +18,7 @@ version: '2'
services:
thingsboard:
- image: "thingsboard/application:1.2.2"
+ image: "thingsboard/application:1.2.3"
ports:
- "8080:8080"
- "1883:1883"
@@ -27,7 +27,7 @@ services:
- thingsboard.env
entrypoint: ./run_thingsboard.sh
thingsboard-db-schema:
- image: "thingsboard/thingsboard-db-schema:1.2.2"
+ image: "thingsboard/thingsboard-db-schema:1.2.3"
env_file:
- thingsboard-db-schema.env
entrypoint: ./install_schema.sh
docker/zookeeper/zkOk.sh 2(+1 -1)
diff --git a/docker/zookeeper/zkOk.sh b/docker/zookeeper/zkOk.sh
index 6dd38ca..449a0fd 100755
--- a/docker/zookeeper/zkOk.sh
+++ b/docker/zookeeper/zkOk.sh
@@ -25,4 +25,4 @@ if [ "$OK" == "imok" ]; then
exit 0
else
exit 1
-fi
\ No newline at end of file
+fi
extensions/extension-kafka/pom.xml 2(+1 -1)
diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml
index 9ec8238..6c35bd2 100644
--- a/extensions/extension-kafka/pom.xml
+++ b/extensions/extension-kafka/pom.xml
@@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml
index 93503e2..941165d 100644
--- a/extensions/extension-rabbitmq/pom.xml
+++ b/extensions/extension-rabbitmq/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml
index 9646e36..a732dc8 100644
--- a/extensions/extension-rest-api-call/pom.xml
+++ b/extensions/extension-rest-api-call/pom.xml
@@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
extensions/pom.xml 2(+1 -1)
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 63b34cf..2c1fdfe 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
extensions-api/pom.xml 2(+1 -1)
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
index b99d274..87ad867 100644
--- a/extensions-api/pom.xml
+++ b/extensions-api/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
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 7b7d0ec..b1288d9 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 988410d..2477ac3 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<ServerAddress> resolve(DeviceId deviceId);
+ Optional<ServerAddress> resolve(EntityId entityId);
void getDevice(DeviceId deviceId, PluginCallback<Device> pluginCallback);
@@ -77,33 +77,33 @@ public interface PluginContext {
*/
- void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback<Void> callback);
+ void saveTsData(EntityId entityId, TsKvEntry entry, PluginCallback<Void> callback);
- void saveTsData(DeviceId deviceId, List<TsKvEntry> entry, PluginCallback<Void> callback);
+ void saveTsData(EntityId entityId, List<TsKvEntry> entry, PluginCallback<Void> callback);
- void loadTimeseries(DeviceId deviceId, List<TsKvQuery> queries, PluginCallback<List<TsKvEntry>> callback);
+ void loadTimeseries(EntityId entityId, List<TsKvQuery> queries, PluginCallback<List<TsKvEntry>> callback);
- void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback);
+ void loadLatestTimeseries(EntityId entityId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback);
- void loadLatestTimeseries(DeviceId deviceId, PluginCallback<List<TsKvEntry>> callback);
+ void loadLatestTimeseries(EntityId entityId, PluginCallback<List<TsKvEntry>> callback);
/*
Attributes API
*/
- void saveAttributes(TenantId tenantId, DeviceId deviceId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
+ void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
- void removeAttributes(TenantId tenantId, DeviceId deviceId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);
+ void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);
- void loadAttribute(DeviceId deviceId, String attributeType, String attributeKey, PluginCallback<Optional<AttributeKvEntry>> callback);
+ void loadAttribute(EntityId entityId, String attributeType, String attributeKey, PluginCallback<Optional<AttributeKvEntry>> callback);
- void loadAttributes(DeviceId deviceId, String attributeType, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
+ void loadAttributes(EntityId entityId, String attributeType, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
- void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback);
+ void loadAttributes(EntityId entityId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback);
- void loadAttributes(DeviceId deviceId, Collection<String> attributeTypes, PluginCallback<List<AttributeKvEntry>> callback);
+ void loadAttributes(EntityId entityId, Collection<String> attributeTypes, PluginCallback<List<AttributeKvEntry>> callback);
- void loadAttributes(DeviceId deviceId, Collection<String> attributeTypes, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
+ void loadAttributes(EntityId entityId, Collection<String> attributeTypes, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback<List<Device>> callback);
}
extensions-core/pom.xml 7(+6 -1)
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index 9ccc087..923d474 100644
--- a/extensions-core/pom.xml
+++ b/extensions-core/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
@@ -42,6 +42,11 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>org.thingsboard.common</groupId>
+ <artifactId>transport</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
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 145f8c4..9555dd6 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 3574eae..0b69b89 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 0000000..d7340c6
--- /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 633f620..5b32474 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -16,13 +16,19 @@
package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
import com.fasterxml.jackson.databind.JsonNode;
+import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler;
@@ -50,121 +56,116 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
public void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
RestRequest request = msg.getRequest();
String[] pathParams = request.getPathParams();
- if (pathParams.length >= 3) {
- String deviceIdStr = pathParams[0];
- String method = pathParams[1];
- String entity = pathParams[2];
- String scope = pathParams.length >= 4 ? pathParams[3] : null;
- if (StringUtils.isEmpty(method) || StringUtils.isEmpty(entity) || StringUtils.isEmpty(deviceIdStr)) {
- msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
- return;
- }
+ if (pathParams.length < 4) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ return;
+ }
- DeviceId deviceId = DeviceId.fromString(deviceIdStr);
+ String entityType = pathParams[0];
+ String entityIdStr = pathParams[1];
+ String method = pathParams[2];
+ TelemetryFeature feature = TelemetryFeature.forName(pathParams[3]);
+ String scope = pathParams.length >= 5 ? pathParams[4] : null;
+ if (StringUtils.isEmpty(entityType) || EntityType.valueOf(entityType) == null || StringUtils.isEmpty(entityIdStr) || StringUtils.isEmpty(method) || StringUtils.isEmpty(feature)) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ return;
+ }
- if (method.equals("keys")) {
- if (entity.equals("timeseries")) {
- ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
- @Override
- public void onSuccess(PluginContext ctx, List<TsKvEntry> value) {
- List<String> 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<List<AttributeKvEntry>> 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<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(PluginContext ctx, List<TsKvEntry> value) {
+ List<String> 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<List<AttributeKvEntry>> 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<String> keys = Arrays.asList(keysStr.split(","));
+ }
+ } else if (method.equals("values")) {
+ if (feature == TelemetryFeature.TIMESERIES) {
+ String keysStr = request.getParameter("keys");
+ List<String> keys = Arrays.asList(keysStr.split(","));
- Optional<Long> startTs = request.getLongParamValue("startTs");
- Optional<Long> endTs = request.getLongParamValue("endTs");
- Optional<Long> interval = request.getLongParamValue("interval");
- Optional<Integer> limit = request.getIntParamValue("limit");
+ Optional<Long> startTs = request.getLongParamValue("startTs");
+ Optional<Long> endTs = request.getLongParamValue("endTs");
+ Optional<Long> interval = request.getLongParamValue("interval");
+ Optional<Integer> 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<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
+ .collect(Collectors.toList());
+ ctx.loadTimeseries(entityId, queries, getTsKvListCallback(msg));
+ } else {
+ ctx.loadLatestTimeseries(entityId, keys, getTsKvListCallback(msg));
+ }
+ } else if (feature == TelemetryFeature.ATTRIBUTES) {
+ String keys = request.getParameter("keys", "");
- List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
- .collect(Collectors.toList());
- ctx.loadTimeseries(deviceId, queries, getTsKvListCallback(msg));
+ PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
+ if (!StringUtils.isEmpty(scope)) {
+ if (!StringUtils.isEmpty(keys)) {
+ List<String> 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<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
- if (!StringUtils.isEmpty(scope)) {
- if (!StringUtils.isEmpty(keys)) {
- List<String> keyList = Arrays.asList(keys.split(","));
- ctx.loadAttributes(deviceId, scope, keyList, callback);
- } else {
- ctx.loadAttributes(deviceId, scope, callback);
- }
+ } else {
+ if (!StringUtils.isEmpty(keys)) {
+ List<String> keyList = Arrays.asList(keys.split(","));
+ ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), keyList, callback);
} else {
- if (!StringUtils.isEmpty(keys)) {
- List<String> 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<List<TsKvEntry>> getTsKvListCallback(final PluginRestMsg msg) {
- return new PluginCallback<List<TsKvEntry>>() {
- @Override
- public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
- Map<String, List<TsData>> result = new LinkedHashMap<>();
- for (TsKvEntry entry : data) {
- List<TsData> vList = result.get(entry.getKey());
- if (vList == null) {
- vList = new ArrayList<>();
- result.put(entry.getKey(), vList);
- }
- vList.add(new TsData(entry.getTs(), entry.getValueAsString()));
- }
- msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK));
- }
-
- @Override
- public void onFailure(PluginContext ctx, Exception e) {
- log.error("Failed to fetch historical data", e);
- msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
- }
- };
- }
-
@Override
public void handleHttpPostRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
RestRequest request = msg.getRequest();
try {
String[] pathParams = request.getPathParams();
+ EntityId entityId;
+ String scope;
+ TelemetryFeature feature;
if (pathParams.length == 2) {
- DeviceId deviceId = DeviceId.fromString(pathParams[0]);
- String scope = pathParams[1];
+ entityId = DeviceId.fromString(pathParams[0]);
+ scope = pathParams[1];
+ feature = TelemetryFeature.ATTRIBUTES;
+ } else if (pathParams.length == 3) {
+ entityId = EntityIdFactory.getByTypeAndId(pathParams[0], pathParams[1]);
+ scope = pathParams[2];
+ feature = TelemetryFeature.ATTRIBUTES;
+ } else if (pathParams.length == 4) {
+ entityId = EntityIdFactory.getByTypeAndId(pathParams[0], pathParams[1]);
+ feature = TelemetryFeature.forName(pathParams[2].toUpperCase());
+ scope = pathParams[3];
+ } else {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+ return;
+ }
+ if (feature == TelemetryFeature.ATTRIBUTES) {
if (DataConstants.SERVER_SCOPE.equals(scope) ||
DataConstants.SHARED_SCOPE.equals(scope)) {
JsonNode jsonNode = jsonMapper.readTree(request.getRequestBody());
@@ -185,11 +186,11 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
}
});
if (attributes.size() > 0) {
- ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, attributes, new PluginCallback<Void>() {
+ ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), entityId, scope, attributes, new PluginCallback<Void>() {
@Override
public void onSuccess(PluginContext ctx, Void value) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
- subscriptionManager.onAttributesUpdateFromServer(ctx, deviceId, scope, attributes);
+ subscriptionManager.onAttributesUpdateFromServer(ctx, entityId, scope, attributes);
}
@Override
@@ -202,6 +203,28 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
}
}
}
+ } else if (feature == TelemetryFeature.TIMESERIES) {
+ TelemetryUploadRequest telemetryRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(request.getRequestBody()));
+ List<TsKvEntry> entries = new ArrayList<>();
+ for (Map.Entry<Long, List<KvEntry>> entry : telemetryRequest.getData().entrySet()) {
+ for (KvEntry kv : entry.getValue()) {
+ entries.add(new BasicTsKvEntry(entry.getKey(), kv));
+ }
+ }
+ ctx.saveTsData(entityId, entries, new PluginCallback<Void>() {
+ @Override
+ public void onSuccess(PluginContext ctx, Void value) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+ subscriptionManager.onTimeseriesUpdateFromServer(ctx, entityId, entries);
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ log.error("Failed to save attributes", e);
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ });
+ return;
}
} catch (IOException | RuntimeException e) {
log.debug("Failed to process POST request due to exception", e);
@@ -214,29 +237,38 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
RestRequest request = msg.getRequest();
try {
String[] pathParams = request.getPathParams();
+ EntityId entityId;
+ String scope;
if (pathParams.length == 2) {
- DeviceId deviceId = DeviceId.fromString(pathParams[0]);
- String scope = pathParams[1];
- if (DataConstants.SERVER_SCOPE.equals(scope) ||
- DataConstants.SHARED_SCOPE.equals(scope) ||
- DataConstants.CLIENT_SCOPE.equals(scope)) {
- String keysParam = request.getParameter("keys");
- if (!StringUtils.isEmpty(keysParam)) {
- String[] keys = keysParam.split(",");
- ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, Arrays.asList(keys), new PluginCallback<Void>() {
- @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<Void>() {
+ @Override
+ public void onSuccess(PluginContext ctx, Void value) {
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+ }
+
+ @Override
+ public void onFailure(PluginContext ctx, Exception e) {
+ log.error("Failed to remove attributes", e);
+ msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ });
+ return;
}
}
} catch (RuntimeException e) {
@@ -278,4 +310,29 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
}
};
}
+
+
+ private PluginCallback<List<TsKvEntry>> getTsKvListCallback(final PluginRestMsg msg) {
+ return new PluginCallback<List<TsKvEntry>>() {
+ @Override
+ public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
+ Map<String, List<TsData>> result = new LinkedHashMap<>();
+ for (TsKvEntry entry : data) {
+ List<TsData> 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 a0fc9fc..a1b734e 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<String, Long> 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<AttributeKvEntry> 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<AttributeKvEntry> attributes) {
+ ctx.sendPluginRpcMsg(new RpcMsg(address, ATTRIBUTES_UPDATE_CLAZZ, getAttributesUpdateProto(entityId, scope, attributes).toByteArray()));
}
- private AttributeUpdateProto getAttributesUpdateProto(DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
+ public void onTimeseriesUpdate(PluginContext ctx, ServerAddress address, EntityId entityId, List<TsKvEntry> tsKvEntries) {
+ ctx.sendPluginRpcMsg(new RpcMsg(address, TIMESERIES_UPDATE_CLAZZ, getTimeseriesUpdateProto(entityId, tsKvEntries).toByteArray()));
+ }
+
+ private TimeseriesUpdateProto getTimeseriesUpdateProto(EntityId entityId, List<TsKvEntry> 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<AttributeKvEntry> 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 f018aaa..c231ffa 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<Set<String>> 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<List<AttributeKvEntry>> callback = new PluginCallback<List<AttributeKvEntry>>() {
@@ -152,8 +153,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
Map<String, Long> 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<Set<String>> keysOptional = getKeys(cmd);
if (keysOptional.isPresent()) {
long startTs;
if (cmd.getTimeWindow() > 0) {
List<String> 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<TsKvQuery> 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<String> 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<List<TsKvEntry>>() {
+ ctx.loadLatestTimeseries(entityId, new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> 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<List<TsKvEntry>> getSubscriptionCallback(final PluginWebsocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final DeviceId deviceId, final long startTs, final List<String> keys) {
+ private PluginCallback<List<TsKvEntry>> getSubscriptionCallback(final PluginWebsocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List<String> keys) {
return new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
@@ -239,8 +240,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
Map<String, Long> 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<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
- List<TsKvQuery> 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<TsKvEntry>>() {
+ List<TsKvQuery> 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<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> 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 430a559..1285cfa 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 fbadd64..5e15fda 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<String, Long> 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/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
index 8cb1bf3..60f46ce 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<DeviceId, Set<Subscription>> subscriptionsByDeviceId = new HashMap<>();
+ private final Map<EntityId, Set<Subscription>> subscriptionsByEntityId = new HashMap<>();
private final Map<String, Map<Integer, Subscription>> 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<ServerAddress> server = ctx.resolve(deviceId);
+ public void addLocalWsSubscription(PluginContext ctx, String sessionId, EntityId entityId, SubscriptionState sub) {
+ Optional<ServerAddress> 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<String, Long> keyStates = subscription.getKeyStates();
- ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback<List<AttributeKvEntry>>() {
+ ctx.loadAttributes(entityId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> values) {
List<TsKvEntry> 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<List<TsKvEntry>>() {
+ ctx.loadTimeseries(entityId, queries, new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> missedUpdates) {
if (!missedUpdates.isEmpty()) {
@@ -112,11 +113,11 @@ public class SubscriptionManager {
}
- private void registerSubscription(String sessionId, DeviceId deviceId, Subscription subscription) {
- Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(subscription.getDeviceId());
+ private void registerSubscription(String sessionId, EntityId entityId, Subscription subscription) {
+ Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(subscription.getEntityId());
if (deviceSubscriptions == null) {
deviceSubscriptions = new HashSet<>();
- subscriptionsByDeviceId.put(deviceId, deviceSubscriptions);
+ subscriptionsByEntityId.put(entityId, deviceSubscriptions);
}
deviceSubscriptions.add(subscription);
Map<Integer, Subscription> 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<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+ Set<Subscription> 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<Subscription, List<TsKvEntry>> f) {
- Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+ public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, SubscriptionType type, Function<Subscription, List<TsKvEntry>> f) {
+ Set<Subscription> 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<AttributeKvEntry> attributes) {
- Optional<ServerAddress> serverAddress = ctx.resolve(deviceId);
+ public void onAttributesUpdateFromServer(PluginContext ctx, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+ Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
if (!serverAddress.isPresent()) {
- onLocalSubscriptionUpdate(ctx, deviceId, SubscriptionType.ATTRIBUTES, s -> {
+ onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.ATTRIBUTES, s -> {
List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
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<TsKvEntry> entries) {
+ Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
+ if (!serverAddress.isPresent()) {
+ onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.TIMESERIES, s -> {
+ List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
+ 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<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+ EntityId entityId = subscription.getEntityId();
+ Set<Subscription> 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<Map.Entry<DeviceId, Set<Subscription>>> deviceIterator = subscriptionsByDeviceId.entrySet().iterator();
+ Iterator<Map.Entry<EntityId, Set<Subscription>>> deviceIterator = subscriptionsByEntityId.entrySet().iterator();
while (deviceIterator.hasNext()) {
- Map.Entry<DeviceId, Set<Subscription>> e = deviceIterator.next();
+ Map.Entry<EntityId, Set<Subscription>> e = deviceIterator.next();
Set<Subscription> subscriptions = e.getValue();
Optional<ServerAddress> 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/proto/telemetry.proto b/extensions-core/src/main/proto/telemetry.proto
index be97b4f..2bfef59 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;
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 6324712..fe95d9b 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Thingsboard</name>
tools/pom.xml 2(+1 -1)
diff --git a/tools/pom.xml b/tools/pom.xml
index 96235e2..2b83e13 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
transport/coap/pom.xml 2(+1 -1)
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index ef3953d..8f15edc 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
transport/http/pom.xml 2(+1 -1)
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 16e5626..004c57e 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
transport/mqtt/pom.xml 2(+1 -1)
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 15c640c..2de70a7 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index b8de7c5..a78319a 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -82,6 +82,7 @@ public class GatewaySessionCtx {
});
GatewayDeviceSessionCtx ctx = new GatewayDeviceSessionCtx(this, device);
devices.put(deviceName, ctx);
+ log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName);
processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg())));
processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
}
@@ -94,6 +95,9 @@ public class GatewaySessionCtx {
if (deviceSessionCtx != null) {
processor.process(SessionCloseMsg.onDisconnect(deviceSessionCtx.getSessionId()));
deviceSessionCtx.setClosed(true);
+ log.debug("[{}] Removed device [{}] from the gateway session", gatewaySessionId, deviceName);
+ } else {
+ log.debug("[{}] Device [{}] was already removed from the gateway session", gatewaySessionId, deviceName);
}
ack(msg);
}
@@ -191,7 +195,8 @@ public class GatewaySessionCtx {
private String checkDeviceConnected(String deviceName) {
if (!devices.containsKey(deviceName)) {
- throw new RuntimeException("Device is not connected!");
+ log.debug("[{}] Missing device [{}] for the gateway session", gatewaySessionId, deviceName);
+ throw new RuntimeException("Device " + deviceName + " is not connected!");
} else {
return deviceName;
}
transport/pom.xml 2(+1 -1)
diff --git a/transport/pom.xml b/transport/pom.xml
index bca6091..fb47647 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/package.json 2(+1 -1)
diff --git a/ui/package.json b/ui/package.json
index ddcc96e..73d7bcd 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "thingsboard",
"private": true,
- "version": "1.2.3",
+ "version": "1.3.0",
"description": "Thingsboard UI",
"licenses": [
{
ui/pom.xml 2(+1 -1)
diff --git a/ui/pom.xml b/ui/pom.xml
index 1ffaa67..1efb29a 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.2.3-SNAPSHOT</version>
+ <version>1.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/src/app/api/asset.service.js 261(+261 -0)
diff --git a/ui/src/app/api/asset.service.js b/ui/src/app/api/asset.service.js
new file mode 100644
index 0000000..f685b1e
--- /dev/null
+++ b/ui/src/app/api/asset.service.js
@@ -0,0 +1,261 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.asset', [])
+ .factory('assetService', AssetService)
+ .name;
+
+/*@ngInject*/
+function AssetService($http, $q, customerService, userService) {
+
+ var service = {
+ getAsset: getAsset,
+ getAssets: getAssets,
+ saveAsset: saveAsset,
+ deleteAsset: deleteAsset,
+ assignAssetToCustomer: assignAssetToCustomer,
+ unassignAssetFromCustomer: unassignAssetFromCustomer,
+ makeAssetPublic: makeAssetPublic,
+ getTenantAssets: getTenantAssets,
+ getCustomerAssets: getCustomerAssets,
+ findByQuery: findByQuery,
+ fetchAssetsByNameFilter: fetchAssetsByNameFilter
+ }
+
+ return service;
+
+ function getAsset(assetId, ignoreErrors, config) {
+ var deferred = $q.defer();
+ var url = '/api/asset/' + assetId;
+ if (!config) {
+ config = {};
+ }
+ config = Object.assign(config, { ignoreErrors: ignoreErrors });
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getAssets(assetIds, config) {
+ var deferred = $q.defer();
+ var ids = '';
+ for (var i=0;i<assetIds.length;i++) {
+ if (i>0) {
+ 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;
+ }
+
+}
ui/src/app/api/attribute.service.js 282(+282 -0)
diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js
new file mode 100644
index 0000000..53d5341
--- /dev/null
+++ b/ui/src/app/api/attribute.service.js
@@ -0,0 +1,282 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.attribute', [])
+ .factory('attributeService', AttributeService)
+ .name;
+
+/*@ngInject*/
+function AttributeService($http, $q, $filter, types, telemetryWebsocketService) {
+
+ var entityAttributesSubscriptionMap = {};
+
+ var service = {
+ getEntityKeys: getEntityKeys,
+ getEntityTimeseriesValues: getEntityTimeseriesValues,
+ getEntityAttributesValues: getEntityAttributesValues,
+ getEntityAttributes: getEntityAttributes,
+ subscribeForEntityAttributes: subscribeForEntityAttributes,
+ unsubscribeForEntityAttributes: unsubscribeForEntityAttributes,
+ saveEntityAttributes: saveEntityAttributes,
+ deleteEntityAttributes: deleteEntityAttributes
+ }
+
+ return service;
+
+ function getEntityKeys(entityType, entityId, query, type) {
+ var deferred = $q.defer();
+ var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/keys/';
+ if (type === types.dataKeyType.timeseries) {
+ url += 'timeseries';
+ } else if (type === types.dataKeyType.attribute) {
+ url += 'attributes';
+ }
+ $http.get(url, null).then(function success(response) {
+ var result = [];
+ if (response.data) {
+ if (query) {
+ var dataKeys = response.data;
+ var lowercaseQuery = angular.lowercase(query);
+ for (var i=0; i<dataKeys.length;i++) {
+ if (angular.lowercase(dataKeys[i]).indexOf(lowercaseQuery) === 0) {
+ result.push(dataKeys[i]);
+ }
+ }
+ } else {
+ result = response.data;
+ }
+ }
+ deferred.resolve(result);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function getEntityTimeseriesValues(entityType, entityId, keys, startTs, endTs, limit) {
+ var deferred = $q.defer();
+ var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/values/timeseries';
+ url += '?keys=' + keys;
+ url += '&startTs=' + startTs;
+ url += '&endTs=' + endTs;
+ if (angular.isDefined(limit)) {
+ url += '&limit=' + limit;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function getEntityAttributesValues(entityType, entityId, attributeScope, keys, config) {
+ var deferred = $q.defer();
+ var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/values/attributes/' + attributeScope;
+ if (keys && keys.length) {
+ url += '?keys=' + keys;
+ }
+ $http.get(url, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function processAttributes(attributes, query, deferred, successCallback, update, apply) {
+ attributes = $filter('orderBy')(attributes, query.order);
+ if (query.search != null) {
+ attributes = $filter('filter')(attributes, {key: query.search});
+ }
+ var responseData = {
+ count: attributes.length
+ }
+ var startIndex = query.limit * (query.page - 1);
+ responseData.data = attributes.slice(startIndex, startIndex + query.limit);
+ successCallback(responseData, update, apply);
+ if (deferred) {
+ deferred.resolve();
+ }
+ }
+
+ function getEntityAttributes(entityType, entityId, attributeScope, query, successCallback, config) {
+ var deferred = $q.defer();
+ var subscriptionId = entityType + entityId + attributeScope;
+ var eas = entityAttributesSubscriptionMap[subscriptionId];
+ if (eas) {
+ if (eas.attributes) {
+ processAttributes(eas.attributes, query, deferred, successCallback);
+ eas.subscriptionCallback = function(attributes) {
+ processAttributes(attributes, query, null, successCallback, true, true);
+ }
+ } else {
+ eas.subscriptionCallback = function(attributes) {
+ processAttributes(attributes, query, deferred, successCallback, false, true);
+ eas.subscriptionCallback = function(attributes) {
+ processAttributes(attributes, query, null, successCallback, true, true);
+ }
+ }
+ }
+ } else {
+ var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/values/attributes/' + attributeScope;
+ $http.get(url, config).then(function success(response) {
+ processAttributes(response.data, query, deferred, successCallback);
+ }, function fail() {
+ deferred.reject();
+ });
+ }
+ return deferred;
+ }
+
+ function onSubscriptionData(data, subscriptionId) {
+ var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId];
+ if (entityAttributesSubscription) {
+ if (!entityAttributesSubscription.attributes) {
+ entityAttributesSubscription.attributes = [];
+ entityAttributesSubscription.keys = {};
+ }
+ var attributes = entityAttributesSubscription.attributes;
+ var keys = entityAttributesSubscription.keys;
+ for (var key in data) {
+ var index = keys[key];
+ var attribute;
+ if (index > -1) {
+ attribute = attributes[index];
+ } else {
+ attribute = {
+ key: key
+ };
+ index = attributes.push(attribute)-1;
+ keys[key] = index;
+ }
+ var attrData = data[key][0];
+ attribute.lastUpdateTs = attrData[0];
+ attribute.value = attrData[1];
+ }
+ if (entityAttributesSubscription.subscriptionCallback) {
+ entityAttributesSubscription.subscriptionCallback(attributes);
+ }
+ }
+ }
+
+ function subscribeForEntityAttributes(entityType, entityId, attributeScope) {
+ var subscriptionId = entityType + entityId + attributeScope;
+ var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId];
+ if (!entityAttributesSubscription) {
+ var subscriptionCommand = {
+ entityType: entityType,
+ entityId: entityId,
+ scope: attributeScope
+ };
+
+ var type = attributeScope === types.latestTelemetry.value ?
+ types.dataKeyType.timeseries : types.dataKeyType.attribute;
+
+ var subscriber = {
+ subscriptionCommand: subscriptionCommand,
+ type: type,
+ onData: function (data) {
+ if (data.data) {
+ onSubscriptionData(data.data, subscriptionId);
+ }
+ }
+ };
+ entityAttributesSubscription = {
+ subscriber: subscriber,
+ attributes: null
+ };
+ entityAttributesSubscriptionMap[subscriptionId] = entityAttributesSubscription;
+ telemetryWebsocketService.subscribe(subscriber);
+ }
+ return subscriptionId;
+ }
+
+ function unsubscribeForEntityAttributes(subscriptionId) {
+ var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId];
+ if (entityAttributesSubscription) {
+ telemetryWebsocketService.unsubscribe(entityAttributesSubscription.subscriber);
+ delete entityAttributesSubscriptionMap[subscriptionId];
+ }
+ }
+
+ function saveEntityAttributes(entityType, entityId, attributeScope, attributes) {
+ var deferred = $q.defer();
+ var attributesData = {};
+ var deleteAttributes = [];
+ for (var a=0; a<attributes.length;a++) {
+ if (angular.isDefined(attributes[a].value) && attributes[a].value !== null) {
+ attributesData[attributes[a].key] = attributes[a].value;
+ } else {
+ deleteAttributes.push(attributes[a]);
+ }
+ }
+ var deleteEntityAttributesPromise;
+ if (deleteAttributes.length) {
+ deleteEntityAttributesPromise = deleteEntityAttributes(entityType, entityId, attributeScope, deleteAttributes);
+ }
+ if (Object.keys(attributesData).length) {
+ var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope;
+ $http.post(url, attributesData).then(function success(response) {
+ if (deleteEntityAttributesPromise) {
+ deleteEntityAttributesPromise.then(
+ function success() {
+ deferred.resolve(response.data);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ )
+ } else {
+ deferred.resolve(response.data);
+ }
+ }, function fail() {
+ deferred.reject();
+ });
+ } else if (deleteEntityAttributesPromise) {
+ deleteEntityAttributesPromise.then(
+ function success() {
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ )
+ } else {
+ deferred.resolve();
+ }
+ return deferred.promise;
+ }
+
+ function deleteEntityAttributes(entityType, entityId, attributeScope, attributes) {
+ var deferred = $q.defer();
+ var keys = '';
+ for (var i = 0; i < attributes.length; i++) {
+ if (i > 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
ui/src/app/api/dashboard.service.js 35(+34 -1)
diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js
index b2a7897..743fbe9 100644
--- a/ui/src/app/api/dashboard.service.js
+++ b/ui/src/app/api/dashboard.service.js
@@ -24,6 +24,8 @@ function DashboardService($http, $q, $location, customerService) {
getCustomerDashboards: getCustomerDashboards,
getServerTimeDiff: getServerTimeDiff,
getDashboard: getDashboard,
+ getDashboardInfo: getDashboardInfo,
+ getTenantDashboardsByTenantId: getTenantDashboardsByTenantId,
getTenantDashboards: getTenantDashboards,
deleteDashboard: deleteDashboard,
saveDashboard: saveDashboard,
@@ -34,6 +36,26 @@ function DashboardService($http, $q, $location, customerService) {
return service;
+ function getTenantDashboardsByTenantId(tenantId, pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/tenant/' + tenantId + '/dashboards?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
function getTenantDashboards(pageLink) {
var deferred = $q.defer();
var url = '/api/tenant/dashboards?limit=' + pageLink.limit;
@@ -94,7 +116,7 @@ function DashboardService($http, $q, $location, customerService) {
var deferred = $q.defer();
var url = '/api/dashboard/serverTime';
var ct1 = Date.now();
- $http.get(url, null).then(function success(response) {
+ $http.get(url, { ignoreLoading: true }).then(function success(response) {
var ct2 = Date.now();
var st = response.data;
var stDiff = Math.ceil(st - (ct1+ct2)/2);
@@ -116,6 +138,17 @@ function DashboardService($http, $q, $location, customerService) {
return deferred.promise;
}
+ function getDashboardInfo(dashboardId) {
+ var deferred = $q.defer();
+ var url = '/api/dashboard/info/' + dashboardId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
function saveDashboard(dashboard) {
var deferred = $q.defer();
var url = '/api/dashboard';
ui/src/app/api/datasource.service.js 25(+14 -11)
diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js
index bf9a057..27da468 100644
--- a/ui/src/app/api/datasource.service.js
+++ b/ui/src/app/api/datasource.service.js
@@ -35,11 +35,10 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t
return service;
-
function subscribeToDatasource(listener) {
var datasource = listener.datasource;
- if (datasource.type === types.datasourceType.device && !listener.deviceId) {
+ if (datasource.type === types.datasourceType.entity && (!listener.entityId || !listener.entityType)) {
return;
}
@@ -64,8 +63,9 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t
if (listener.subscriptionType === types.widgetType.timeseries.value) {
datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow);
}
- if (datasourceSubscription.datasourceType === types.datasourceType.device) {
- datasourceSubscription.deviceId = listener.deviceId;
+ if (datasourceSubscription.datasourceType === types.datasourceType.entity) {
+ datasourceSubscription.entityType = listener.entityType;
+ datasourceSubscription.entityId = listener.entityId;
}
listener.datasourceSubscriptionKey = utils.objectHashCode(datasourceSubscription);
@@ -141,7 +141,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody);
}
}
- if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
+ if (datasourceType === types.datasourceType.entity || datasourceSubscription.type === types.widgetType.timeseries.value) {
if (datasourceType === types.datasourceType.function) {
key = dataKey.name + '_' + dataKey.index + '_' + dataKey.type;
} else {
@@ -191,7 +191,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
function syncListener(listener) {
var key;
var dataKey;
- if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
+ if (datasourceType === types.datasourceType.entity || datasourceSubscription.type === types.widgetType.timeseries.value) {
for (key in dataKeys) {
var dataKeysList = dataKeys[key];
for (var i = 0; i < dataKeysList.length; i++) {
@@ -220,7 +220,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
var tsKeyNames = [];
var dataKey;
- if (datasourceType === types.datasourceType.device) {
+ if (datasourceType === types.datasourceType.entity) {
//send subscribe command
@@ -252,7 +252,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
if (history) {
var historyCommand = {
- deviceId: datasourceSubscription.deviceId,
+ entityType: datasourceSubscription.entityType,
+ entityId: datasourceSubscription.entityId,
keys: tsKeys,
startTs: subsTw.fixedWindow.startTimeMs,
endTs: subsTw.fixedWindow.endTimeMs,
@@ -282,7 +283,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
} else {
subscriptionCommand = {
- deviceId: datasourceSubscription.deviceId,
+ entityType: datasourceSubscription.entityType,
+ entityId: datasourceSubscription.entityId,
keys: tsKeys
};
@@ -328,7 +330,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
if (attrKeys.length > 0) {
subscriptionCommand = {
- deviceId: datasourceSubscription.deviceId,
+ entityType: datasourceSubscription.entityType,
+ entityId: datasourceSubscription.entityId,
keys: attrKeys
};
@@ -404,7 +407,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
$timeout.cancel(timer);
timer = null;
}
- if (datasourceType === types.datasourceType.device) {
+ if (datasourceType === types.datasourceType.entity) {
for (var cmdId in subscribers) {
var subscriber = subscribers[cmdId];
telemetryWebsocketService.unsubscribe(subscriber);
ui/src/app/api/device.service.js 382(+24 -358)
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index ea197a1..d12d025 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -20,10 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
.name;
/*@ngInject*/
-function DeviceService($http, $q, $filter, userService, customerService, telemetryWebsocketService, types) {
-
-
- var deviceAttributesSubscriptionMap = {};
+function DeviceService($http, $q, attributeService, customerService, types) {
var service = {
assignDeviceToCustomer: assignDeviceToCustomer,
@@ -31,12 +28,7 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet
getCustomerDevices: getCustomerDevices,
getDevice: getDevice,
getDevices: getDevices,
- processDeviceAliases: processDeviceAliases,
- checkDeviceAlias: checkDeviceAlias,
- fetchAliasDeviceByNameFilter: fetchAliasDeviceByNameFilter,
getDeviceCredentials: getDeviceCredentials,
- getDeviceKeys: getDeviceKeys,
- getDeviceTimeseriesValues: getDeviceTimeseriesValues,
getTenantDevices: getTenantDevices,
saveDevice: saveDevice,
saveDeviceCredentials: saveDeviceCredentials,
@@ -48,7 +40,8 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet
saveDeviceAttributes: saveDeviceAttributes,
deleteDeviceAttributes: deleteDeviceAttributes,
sendOneWayRpcCommand: sendOneWayRpcCommand,
- sendTwoWayRpcCommand: sendTwoWayRpcCommand
+ sendTwoWayRpcCommand: sendTwoWayRpcCommand,
+ findByQuery: findByQuery
}
return service;
@@ -159,174 +152,6 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet
return deferred.promise;
}
- function fetchAliasDeviceByNameFilter(deviceNameFilter, limit, applyCustomersInfo, config) {
- var deferred = $q.defer();
- var user = userService.getCurrentUser();
- var promise;
- var pageLink = {limit: limit, textSearch: deviceNameFilter};
- if (user.authority === 'CUSTOMER_USER') {
- var customerId = user.customerId;
- promise = getCustomerDevices(customerId, pageLink, applyCustomersInfo, config);
- } else {
- promise = getTenantDevices(pageLink, applyCustomersInfo, config);
- }
- promise.then(
- function success(result) {
- if (result.data && result.data.length > 0) {
- deferred.resolve(result.data);
- } else {
- deferred.resolve(null);
- }
- },
- function fail() {
- deferred.resolve(null);
- }
- );
- return deferred.promise;
- }
-
- function deviceToDeviceInfo(device) {
- return { name: device.name, id: device.id.id };
- }
-
- function devicesToDevicesInfo(devices) {
- var devicesInfo = [];
- for (var d = 0; d < devices.length; d++) {
- devicesInfo.push(deviceToDeviceInfo(devices[d]));
- }
- return devicesInfo;
- }
-
- function processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred) {
- if (index < aliasIds.length) {
- var aliasId = aliasIds[index];
- var deviceAlias = deviceAliases[aliasId];
- var alias = deviceAlias.alias;
- if (!deviceAlias.deviceFilter) {
- getDevice(deviceAlias.deviceId).then(
- function success(device) {
- var resolvedAlias = {alias: alias, deviceId: device.id.id};
- resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
- resolution.aliasesInfo.deviceAliasesInfo[aliasId] = [
- deviceToDeviceInfo(device)
- ];
- index++;
- processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
- },
- function fail() {
- if (!resolution.error) {
- resolution.error = 'dashboard.invalid-aliases-config';
- }
- index++;
- processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
- }
- );
- } else {
- var deviceFilter = deviceAlias.deviceFilter;
- if (deviceFilter.useFilter) {
- var deviceNameFilter = deviceFilter.deviceNameFilter;
- fetchAliasDeviceByNameFilter(deviceNameFilter, 100, false).then(
- function(devices) {
- if (devices && devices != null) {
- var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
- resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
- resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
- index++;
- processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
- } else {
- if (!resolution.error) {
- resolution.error = 'dashboard.invalid-aliases-config';
- }
- index++;
- processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
- }
- });
- } else {
- var deviceList = deviceFilter.deviceList;
- getDevices(deviceList).then(
- function success(devices) {
- if (devices && devices.length > 0) {
- var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
- resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
- resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
- index++;
- processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
- } else {
- if (!resolution.error) {
- resolution.error = 'dashboard.invalid-aliases-config';
- }
- index++;
- processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
- }
- },
- function fail() {
- if (!resolution.error) {
- resolution.error = 'dashboard.invalid-aliases-config';
- }
- index++;
- processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
- }
- );
- }
- }
- } else {
- deferred.resolve(resolution);
- }
- }
-
- function processDeviceAliases(deviceAliases) {
- var deferred = $q.defer();
- var resolution = {
- aliasesInfo: {
- deviceAliases: {},
- deviceAliasesInfo: {}
- }
- };
- var aliasIds = [];
- if (deviceAliases) {
- for (var aliasId in deviceAliases) {
- aliasIds.push(aliasId);
- }
- }
- processDeviceAlias(0, aliasIds, deviceAliases, resolution, deferred);
- return deferred.promise;
- }
-
- function checkDeviceAlias(deviceAlias) {
- var deferred = $q.defer();
- var deviceFilter;
- if (deviceAlias.deviceId) {
- deviceFilter = {
- useFilter: false,
- deviceNameFilter: '',
- deviceList: [deviceAlias.deviceId]
- }
- } else {
- deviceFilter = deviceAlias.deviceFilter;
- }
- var promise;
- if (deviceFilter.useFilter) {
- var deviceNameFilter = deviceFilter.deviceNameFilter;
- promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1, false);
- } else {
- var deviceList = deviceFilter.deviceList;
- promise = getDevices(deviceList);
- }
- promise.then(
- function success(devices) {
- if (devices && devices.length > 0) {
- deferred.resolve(true);
- } else {
- deferred.resolve(false);
- }
- },
- function fail() {
- deferred.resolve(false);
- }
- );
- return deferred.promise;
- }
-
function saveDevice(device) {
var deferred = $q.defer();
var url = '/api/device';
@@ -404,198 +229,24 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet
return deferred.promise;
}
- function getDeviceKeys(deviceId, query, type) {
- var deferred = $q.defer();
- var url = '/api/plugins/telemetry/' + deviceId + '/keys/';
- if (type === types.dataKeyType.timeseries) {
- url += 'timeseries';
- } else if (type === types.dataKeyType.attribute) {
- url += 'attributes';
- }
- $http.get(url, null).then(function success(response) {
- var result = [];
- if (response.data) {
- if (query) {
- var dataKeys = response.data;
- var lowercaseQuery = angular.lowercase(query);
- for (var i=0; i<dataKeys.length;i++) {
- if (angular.lowercase(dataKeys[i]).indexOf(lowercaseQuery) === 0) {
- result.push(dataKeys[i]);
- }
- }
- } else {
- result = response.data;
- }
- }
- deferred.resolve(result);
- }, function fail(response) {
- deferred.reject(response.data);
- });
- return deferred.promise;
- }
-
- function getDeviceTimeseriesValues(deviceId, keys, startTs, endTs, limit) {
- var deferred = $q.defer();
- var url = '/api/plugins/telemetry/' + deviceId + '/values/timeseries';
- url += '?keys=' + keys;
- url += '&startTs=' + startTs;
- url += '&endTs=' + endTs;
- if (angular.isDefined(limit)) {
- url += '&limit=' + limit;
- }
- $http.get(url, null).then(function success(response) {
- deferred.resolve(response.data);
- }, function fail(response) {
- deferred.reject(response.data);
- });
- return deferred.promise;
- }
-
- function processDeviceAttributes(attributes, query, deferred, successCallback, update, apply) {
- attributes = $filter('orderBy')(attributes, query.order);
- if (query.search != null) {
- attributes = $filter('filter')(attributes, {key: query.search});
- }
- var responseData = {
- count: attributes.length
- }
- var startIndex = query.limit * (query.page - 1);
- responseData.data = attributes.slice(startIndex, startIndex + query.limit);
- successCallback(responseData, update, apply);
- if (deferred) {
- deferred.resolve();
- }
- }
-
function getDeviceAttributes(deviceId, attributeScope, query, successCallback, config) {
- var deferred = $q.defer();
- var subscriptionId = deviceId + attributeScope;
- var das = deviceAttributesSubscriptionMap[subscriptionId];
- if (das) {
- if (das.attributes) {
- processDeviceAttributes(das.attributes, query, deferred, successCallback);
- das.subscriptionCallback = function(attributes) {
- processDeviceAttributes(attributes, query, null, successCallback, true, true);
- }
- } else {
- das.subscriptionCallback = function(attributes) {
- processDeviceAttributes(attributes, query, deferred, successCallback, false, true);
- das.subscriptionCallback = function(attributes) {
- processDeviceAttributes(attributes, query, null, successCallback, true, true);
- }
- }
- }
- } else {
- var url = '/api/plugins/telemetry/' + deviceId + '/values/attributes/' + attributeScope;
- $http.get(url, config).then(function success(response) {
- processDeviceAttributes(response.data, query, deferred, successCallback);
- }, function fail() {
- deferred.reject();
- });
- }
- return deferred;
- }
-
- function onSubscriptionData(data, subscriptionId) {
- var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
- if (deviceAttributesSubscription) {
- if (!deviceAttributesSubscription.attributes) {
- deviceAttributesSubscription.attributes = [];
- deviceAttributesSubscription.keys = {};
- }
- var attributes = deviceAttributesSubscription.attributes;
- var keys = deviceAttributesSubscription.keys;
- for (var key in data) {
- var index = keys[key];
- var attribute;
- if (index > -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<attributes.length;a++) {
- attributesData[attributes[a].key] = attributes[a].value;
- }
- var url = '/api/plugins/telemetry/' + deviceId + '/' + attributeScope;
- $http.post(url, attributesData).then(function success(response) {
- deferred.resolve(response.data);
- }, function fail(response) {
- deferred.reject(response.data);
- });
- return deferred.promise;
+ return attributeService.saveEntityAttributes(types.entityType.device, deviceId, attributeScope, attributes);
}
function deleteDeviceAttributes(deviceId, attributeScope, attributes) {
- var deferred = $q.defer();
- var keys = '';
- for (var i = 0; i < attributes.length; i++) {
- if (i > 0) {
- keys += ',';
- }
- keys += attributes[i].key;
- }
- var url = '/api/plugins/telemetry/' + deviceId + '/' + attributeScope + '?keys=' + keys;
- $http.delete(url).then(function success() {
- deferred.resolve();
- }, function fail() {
- deferred.reject();
- });
- return deferred.promise;
+ return attributeService.deleteEntityAttributes(types.entityType.device, deviceId, attributeScope, attributes);
}
function sendOneWayRpcCommand(deviceId, requestBody) {
@@ -620,4 +271,19 @@ function DeviceService($http, $q, $filter, userService, customerService, telemet
return deferred.promise;
}
+ function findByQuery(query, ignoreErrors, config) {
+ var deferred = $q.defer();
+ var url = '/api/devices';
+ if (!config) {
+ config = {};
+ }
+ config = Object.assign(config, { ignoreErrors: ignoreErrors });
+ $http.post(url, query, config).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
}
ui/src/app/api/entity.service.js 777(+777 -0)
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
new file mode 100644
index 0000000..43c537c
--- /dev/null
+++ b/ui/src/app/api/entity.service.js
@@ -0,0 +1,777 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardTypes from '../common/types.constant';
+
+export default angular.module('thingsboard.api.entity', [thingsboardTypes])
+ .factory('entityService', EntityService)
+ .name;
+
+/*@ngInject*/
+function EntityService($http, $q, $filter, $translate, userService, deviceService,
+ assetService, tenantService, customerService,
+ ruleService, pluginService, entityRelationService, attributeService, types, utils) {
+ var service = {
+ getEntity: getEntity,
+ getEntities: getEntities,
+ getEntitiesByNameFilter: getEntitiesByNameFilter,
+ entityName: entityName,
+ processEntityAliases: processEntityAliases,
+ getEntityKeys: getEntityKeys,
+ checkEntityAlias: checkEntityAlias,
+ createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo,
+ getRelatedEntities: getRelatedEntities,
+ saveRelatedEntity: saveRelatedEntity,
+ getRelatedEntity: getRelatedEntity,
+ deleteRelatedEntity: deleteRelatedEntity,
+ moveEntity: moveEntity
+ };
+
+ return service;
+
+ function getEntityPromise(entityType, entityId, config) {
+ var promise;
+ switch (entityType) {
+ case types.entityType.device:
+ promise = deviceService.getDevice(entityId, true, config);
+ break;
+ case types.entityType.asset:
+ promise = assetService.getAsset(entityId, true, config);
+ break;
+ case types.entityType.tenant:
+ promise = tenantService.getTenant(entityId);
+ break;
+ case types.entityType.customer:
+ promise = customerService.getCustomer(entityId);
+ break;
+ case types.entityType.rule:
+ promise = ruleService.getRule(entityId);
+ break;
+ case types.entityType.plugin:
+ promise = pluginService.getPlugin(entityId);
+ break;
+ }
+ return promise;
+ }
+
+ function getEntity(entityType, entityId, config) {
+ var deferred = $q.defer();
+ var promise = getEntityPromise(entityType, entityId, config);
+ if (promise) {
+ promise.then(
+ function success(result) {
+ deferred.resolve(result);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+
+ function getEntitiesByIdsPromise(fetchEntityFunction, entityIds) {
+ var tasks = [];
+ var deferred = $q.defer();
+ for (var i=0;i<entityIds.length;i++) {
+ tasks.push(fetchEntityFunction(entityIds[i]));
+ }
+ $q.all(tasks).then(
+ function success(entities) {
+ if (entities) {
+ entities.sort(function (entity1, entity2) {
+ var id1 = entity1.id.id;
+ var id2 = entity2.id.id;
+ var index1 = entityIds.indexOf(id1);
+ var index2 = entityIds.indexOf(id2);
+ return index1 - index2;
+ });
+ deferred.resolve(entities);
+ } else {
+ deferred.resolve([]);
+ }
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getEntitiesPromise(entityType, entityIds, config) {
+ var promise;
+ switch (entityType) {
+ case types.entityType.device:
+ promise = deviceService.getDevices(entityIds, config);
+ break;
+ case types.entityType.asset:
+ promise = assetService.getAssets(entityIds, config);
+ break;
+ case types.entityType.tenant:
+ promise = getEntitiesByIdsPromise(tenantService.getTenant, entityIds);
+ break;
+ case types.entityType.customer:
+ promise = getEntitiesByIdsPromise(customerService.getCustomer, entityIds);
+ break;
+ case types.entityType.rule:
+ promise = getEntitiesByIdsPromise(ruleService.getRule, entityIds);
+ break;
+ case types.entityType.plugin:
+ promise = getEntitiesByIdsPromise(pluginService.getPlugin, entityIds);
+ break;
+ }
+ return promise;
+ }
+
+ function getEntities(entityType, entityIds, config) {
+ var deferred = $q.defer();
+ var promise = getEntitiesPromise(entityType, entityIds, config);
+ promise.then(
+ function success(result) {
+ deferred.resolve(result);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getEntitiesByPageLinkPromise(entityType, pageLink, config) {
+ var promise;
+ var user = userService.getCurrentUser();
+ var customerId = user.customerId;
+ switch (entityType) {
+ case types.entityType.device:
+ if (user.authority === 'CUSTOMER_USER') {
+ promise = deviceService.getCustomerDevices(customerId, pageLink, false, config);
+ } else {
+ promise = deviceService.getTenantDevices(pageLink, false, config);
+ }
+ break;
+ case types.entityType.asset:
+ if (user.authority === 'CUSTOMER_USER') {
+ promise = assetService.getCustomerAssets(customerId, pageLink, false, config);
+ } else {
+ promise = assetService.getTenantAssets(pageLink, false, config);
+ }
+ break;
+ case types.entityType.tenant:
+ promise = tenantService.getTenants(pageLink);
+ break;
+ case types.entityType.customer:
+ promise = customerService.getCustomers(pageLink);
+ break;
+ case types.entityType.rule:
+ promise = ruleService.getAllRules(pageLink);
+ break;
+ case types.entityType.plugin:
+ promise = pluginService.getAllPlugins(pageLink);
+ break;
+ }
+ return promise;
+ }
+
+ function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config) {
+ var deferred = $q.defer();
+ var pageLink = {limit: limit, textSearch: entityNameFilter};
+ var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config);
+ promise.then(
+ function success(result) {
+ if (result.data && result.data.length > 0) {
+ deferred.resolve(result.data);
+ } else {
+ deferred.resolve(null);
+ }
+ },
+ function fail() {
+ deferred.resolve(null);
+ }
+ );
+ return deferred.promise;
+ }
+
+ function 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<dataKeys.length;i++) {
+ if (angular.lowercase(dataKeys[i]).indexOf(lowercaseQuery) === 0) {
+ result.push(dataKeys[i]);
+ }
+ }
+ } else {
+ result = response.data;
+ }
+ }
+ deferred.resolve(result);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function checkEntityAlias(entityAlias) {
+ var deferred = $q.defer();
+ var entityType = entityAlias.entityType;
+ var entityFilter = entityAlias.entityFilter;
+ var promise;
+ if (entityFilter.useFilter) {
+ var entityNameFilter = entityFilter.entityNameFilter;
+ promise = getEntitiesByNameFilter(entityType, entityNameFilter, 1);
+ } else {
+ var entityList = entityFilter.entityList;
+ promise = getEntities(entityType, entityList);
+ }
+ promise.then(
+ function success(entities) {
+ if (entities && entities.length > 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<keyInfos.length;i++) {
+ var keyInfo = keyInfos[i];
+ var dataKey = utils.createKey(keyInfo, type, datasources);
+ datasource.dataKeys.push(dataKey);
+ }
+ }
+
+ function getRelatedEntities(rootEntityId, entityType, entitySubTypes, maxLevel, keys, typeTranslatePrefix) {
+ var deferred = $q.defer();
+
+ var entitySearchQuery = constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel);
+ if (!entitySearchQuery) {
+ deferred.reject();
+ } else {
+ var findByQueryPromise;
+ if (entityType == types.entityType.asset) {
+ findByQueryPromise = assetService.findByQuery(entitySearchQuery, true, {ignoreLoading: true});
+ } else if (entityType == types.entityType.device) {
+ findByQueryPromise = deviceService.findByQuery(entitySearchQuery, true, {ignoreLoading: true});
+ }
+ findByQueryPromise.then(
+ function success(entities) {
+ var entitiesTasks = [];
+ for (var i=0;i<entities.length;i++) {
+ var entity = entities[i];
+ var entityPromise = constructEntity(entity, keys, typeTranslatePrefix);
+ entitiesTasks.push(entityPromise);
+ }
+ $q.all(entitiesTasks).then(
+ function success(entities) {
+ deferred.resolve(entities);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ function saveRelatedEntity(relatedEntity, parentEntityId, keys) {
+ var deferred = $q.defer();
+ if (relatedEntity.id.id) {
+ updateRelatedEntity(relatedEntity, keys, deferred);
+ } else {
+ addRelatedEntity(relatedEntity, parentEntityId, keys, deferred);
+ }
+ return deferred.promise;
+ }
+
+ function getRelatedEntity(entityId, keys, typeTranslatePrefix) {
+ var deferred = $q.defer();
+ getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then(
+ function success(entity) {
+ constructEntity(entity, keys, typeTranslatePrefix).then(
+ function success(relatedEntity) {
+ deferred.resolve(relatedEntity);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function deleteEntityPromise(entityId) {
+ if (entityId.entityType == types.entityType.asset) {
+ return assetService.deleteAsset(entityId.id);
+ } else if (entityId.entityType == types.entityType.device) {
+ return deviceService.deleteDevice(entityId.id);
+ }
+ }
+
+ function deleteRelatedEntity(entityId, deleteRelatedEntityTypes) {
+ var deferred = $q.defer();
+ if (deleteRelatedEntityTypes) {
+ var deleteRelatedEntitiesTasks = [];
+ entityRelationService.findByFrom(entityId.id, entityId.entityType).then(
+ function success(entityRelations) {
+ for (var i=0;i<entityRelations.length;i++) {
+ var entityRelation = entityRelations[i];
+ var relationEntityId = entityRelation.to;
+ if (deleteRelatedEntityTypes.length == 0 || deleteRelatedEntityTypes.indexOf(relationEntityId.entityType) > -1) {
+ var deleteRelatedEntityPromise = deleteRelatedEntity(relationEntityId, deleteRelatedEntityTypes);
+ deleteRelatedEntitiesTasks.push(deleteRelatedEntityPromise);
+ }
+ }
+ deleteRelatedEntitiesTasks.push(deleteEntityPromise(entityId));
+ $q.all(deleteRelatedEntitiesTasks).then(
+ function success() {
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ )
+ } else {
+ deleteEntityPromise(entityId).then(
+ function success() {
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ function moveEntity(entityId, prevParentId, targetParentId) {
+ var deferred = $q.defer();
+ entityRelationService.deleteRelation(prevParentId.id, prevParentId.entityType,
+ types.entityRelationType.contains, entityId.id, entityId.entityType).then(
+ function success() {
+ var relation = {
+ from: targetParentId,
+ to: entityId,
+ type: types.entityRelationType.contains
+ };
+ entityRelationService.saveRelation(relation).then(
+ function success() {
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function saveEntityPromise(entity) {
+ var entityType = entity.id.entityType;
+ if (!entity.id.id) {
+ delete entity.id;
+ }
+ if (entityType == types.entityType.asset) {
+ return assetService.saveAsset(entity);
+ } else if (entityType == types.entityType.device) {
+ return deviceService.saveDevice(entity);
+ }
+ }
+
+ function addRelatedEntity(relatedEntity, parentEntityId, keys, deferred) {
+ var entity = {};
+ entity.id = relatedEntity.id;
+ entity.name = relatedEntity.name;
+ entity.type = relatedEntity.type;
+ saveEntityPromise(entity).then(
+ function success(entity) {
+ relatedEntity.id = entity.id;
+ var relation = {
+ from: parentEntityId,
+ to: relatedEntity.id,
+ type: types.entityRelationType.contains
+ };
+ entityRelationService.saveRelation(relation).then(
+ function success() {
+ updateEntity(entity, relatedEntity, keys, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }
+
+ function updateRelatedEntity(relatedEntity, keys, deferred) {
+ getEntityPromise(relatedEntity.id.entityType, relatedEntity.id.id, {ignoreLoading: true}).then(
+ function success(entity) {
+ updateEntity(entity, relatedEntity, keys, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }
+
+ function updateEntity(entity, relatedEntity, keys, deferred) {
+ if (!angular.equals(entity.name, relatedEntity.name) || !angular.equals(entity.type, relatedEntity.type)) {
+ entity.name = relatedEntity.name;
+ entity.type = relatedEntity.type;
+ saveEntityPromise(entity).then(
+ function success (entity) {
+ updateEntityAttributes(entity, relatedEntity, keys, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ updateEntityAttributes(entity, relatedEntity, keys, deferred);
+ }
+ }
+
+ function updateEntityAttributes(entity, relatedEntity, keys, deferred) {
+ var attributes = [];
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ attributes.push({key: key, value: relatedEntity[key]});
+ }
+ attributeService.saveEntityAttributes(entity.id.entityType, entity.id.id, types.attributesScope.server.value, attributes)
+ .then(
+ function success() {
+ deferred.resolve(relatedEntity);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }
+
+ function constructRelatedEntitiesSearchQuery(rootEntityId, entityType, entitySubTypes, maxLevel) {
+
+ var searchQuery = {
+ parameters: {
+ rootId: rootEntityId.id,
+ rootType: rootEntityId.entityType,
+ direction: types.entitySearchDirection.from
+ },
+ relationType: types.entityRelationType.contains
+ };
+
+ if (maxLevel) {
+ searchQuery.parameters.maxLevel = maxLevel;
+ } else {
+ searchQuery.parameters.maxLevel = 1;
+ }
+
+ if (entityType == types.entityType.asset) {
+ searchQuery.assetTypes = entitySubTypes;
+ } else if (entityType == types.entityType.device) {
+ searchQuery.deviceTypes = entitySubTypes;
+ } else {
+ return null; //Not supported
+ }
+
+ return searchQuery;
+ }
+
+ function constructEntity(entity, keys, typeTranslatePrefix) {
+ var deferred = $q.defer();
+ if (typeTranslatePrefix) {
+ entity.typeName = $translate.instant(typeTranslatePrefix+'.'+entity.type);
+ } else {
+ entity.typeName = entity.type;
+ }
+ attributeService.getEntityAttributesValues(entity.id.entityType, entity.id.id,
+ types.attributesScope.server.value, keys.join(','),
+ {ignoreLoading: true}).then(
+ function success(attributes) {
+ if (attributes && attributes.length > 0) {
+ for (var i=0;i<keys.length;i++) {
+ var key = keys[i];
+ entity[key] = getAttributeValue(attributes, key);
+ }
+ }
+ deferred.resolve(entity);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getAttributeValue(attributes, key) {
+ var foundAttributes = $filter('filter')(attributes, {key: key}, true);
+ if (foundAttributes.length > 0) {
+ return foundAttributes[0].value;
+ } else {
+ return null;
+ }
+ }
+
+}
\ No newline at end of file
ui/src/app/api/entity-relation.service.js 136(+136 -0)
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 0000000..998607a
--- /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;
+ }
+
+}
ui/src/app/api/subscription.js 123(+67 -56)
diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js
index afb9a06..633bcc3 100644
--- a/ui/src/app/api/subscription.js
+++ b/ui/src/app/api/subscription.js
@@ -64,7 +64,7 @@ export default class Subscription {
this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || function(){};
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
- this.datasources = options.datasources;
+ this.datasources = this.ctx.utils.validateDatasources(options.datasources);
this.datasourceListeners = [];
this.data = [];
this.hiddenData = [];
@@ -172,12 +172,6 @@ export default class Subscription {
this.startWatchingTimewindow();
}
}
-
- registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
- subscription.checkSubscriptions();
- });
-
- this.registrations.push(registration);
}
startWatchingTimewindow() {
@@ -204,29 +198,11 @@ export default class Subscription {
}
initRpc() {
-
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
- if (this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId]) {
- this.targetDeviceId = this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId].deviceId;
+ if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) {
+ this.targetDeviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId;
}
- var subscription = this;
- var registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
- var deviceId = null;
- if (subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId]) {
- deviceId = subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId].deviceId;
- }
- if (!angular.equals(deviceId, subscription.targetDeviceId)) {
- subscription.targetDeviceId = deviceId;
- if (subscription.targetDeviceId) {
- subscription.rpcEnabled = true;
- } else {
- subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false;
- }
- subscription.callbacks.rpcStateChanged(subscription);
- }
- });
- this.registrations.push(registration);
} else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) {
this.targetDeviceId = this.targetDeviceIds[0];
}
@@ -342,6 +318,14 @@ export default class Subscription {
this.onDataUpdated();
}
+ onAliasesChanged() {
+ if (this.type === this.ctx.types.widgetType.rpc.value) {
+ this.checkRpcTarget();
+ } else {
+ this.checkSubscriptions();
+ }
+ }
+
onDataUpdated(apply) {
if (this.cafs['dataUpdated']) {
this.cafs['dataUpdated']();
@@ -493,25 +477,30 @@ export default class Subscription {
var datasource = this.datasources[i];
if (angular.isFunction(datasource))
continue;
- var deviceId = null;
- if (datasource.type === this.ctx.types.datasourceType.device) {
+ var entityId = null;
+ var entityType = null;
+ if (datasource.type === this.ctx.types.datasourceType.entity) {
var aliasName = null;
- var deviceName = null;
- if (datasource.deviceId) {
- deviceId = datasource.deviceId;
- datasource.name = datasource.deviceName;
- aliasName = datasource.deviceName;
- deviceName = datasource.deviceName;
- } else if (datasource.deviceAliasId && this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
- deviceId = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
- datasource.name = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
- aliasName = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
- deviceName = '';
- var devicesInfo = this.ctx.aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];
- for (var d = 0; d < devicesInfo.length; d++) {
- if (devicesInfo[d].id === deviceId) {
- deviceName = devicesInfo[d].name;
- break;
+ var entityName = null;
+ if (datasource.entityId) {
+ entityId = datasource.entityId;
+ entityType = datasource.entityType;
+ datasource.name = datasource.entityName;
+ aliasName = datasource.entityName;
+ entityName = datasource.entityName;
+ } else if (datasource.entityAliasId) {
+ if (this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId]) {
+ entityId = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].entityId;
+ entityType = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].entityType;
+ datasource.name = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].alias;
+ aliasName = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].alias;
+ entityName = '';
+ var entitiesInfo = this.ctx.aliasesInfo.entityAliasesInfo[datasource.entityAliasId];
+ for (var d = 0; d < entitiesInfo.length; d++) {
+ if (entitiesInfo[d].id === entityId) {
+ entityName = entitiesInfo[d].name;
+ break;
+ }
}
}
}
@@ -519,7 +508,7 @@ export default class Subscription {
datasource.name = datasource.name || this.ctx.types.datasourceType.function;
}
for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
- updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, deviceName, aliasName);
+ updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, entityName, aliasName);
}
var subscription = this;
@@ -528,7 +517,8 @@ export default class Subscription {
subscriptionType: this.type,
subscriptionTimewindow: this.subscriptionTimewindow,
datasource: datasource,
- deviceId: deviceId,
+ entityType: entityType,
+ entityId: entityId,
dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
},
@@ -563,19 +553,38 @@ export default class Subscription {
}
}
+ checkRpcTarget() {
+ var deviceId = null;
+ if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) {
+ deviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId;
+ }
+ if (!angular.equals(deviceId, this.targetDeviceId)) {
+ this.targetDeviceId = deviceId;
+ if (this.targetDeviceId) {
+ this.rpcEnabled = true;
+ } else {
+ this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
+ }
+ this.callbacks.rpcStateChanged(this);
+ }
+ }
+
checkSubscriptions() {
var subscriptionsChanged = false;
for (var i = 0; i < this.datasourceListeners.length; i++) {
var listener = this.datasourceListeners[i];
- var deviceId = null;
+ var entityId = null;
+ var entityType = null;
var aliasName = null;
- if (listener.datasource.type === this.ctx.types.datasourceType.device) {
- if (listener.datasource.deviceAliasId &&
- this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
- deviceId = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
- aliasName = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
+ if (listener.datasource.type === this.ctx.types.datasourceType.entity) {
+ if (listener.datasource.entityAliasId &&
+ this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId]) {
+ entityId = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].entityId;
+ entityType = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].entityType;
+ aliasName = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].alias;
}
- if (!angular.equals(deviceId, listener.deviceId) ||
+ if (!angular.equals(entityId, listener.entityId) ||
+ !angular.equals(entityType, listener.entityType) ||
!angular.equals(aliasName, listener.datasource.name)) {
subscriptionsChanged = true;
break;
@@ -606,7 +615,7 @@ export default class Subscription {
const varsRegex = /\$\{([^\}]*)\}/g;
-function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) {
+function updateDataKeyLabel(dataKey, dsName, entityName, aliasName) {
var pattern = dataKey.pattern;
var label = dataKey.pattern;
var match = varsRegex.exec(pattern);
@@ -615,8 +624,10 @@ function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) {
var variableName = match[1];
if (variableName === 'dsName') {
label = label.split(variable).join(dsName);
+ } else if (variableName === 'entityName') {
+ label = label.split(variable).join(entityName);
} else if (variableName === 'deviceName') {
- label = label.split(variable).join(deviceName);
+ label = label.split(variable).join(entityName);
} else if (variableName === 'aliasName') {
label = label.split(variable).join(aliasName);
}
ui/src/app/api/user.service.js 15(+11 -4)
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index 4dc41f7..a5bb36c 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -262,7 +262,13 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
function fetchAllowedDashboardIds() {
var pageLink = {limit: 100};
- dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(
+ var fetchDashboardsPromise;
+ if (currentUser.authority === 'TENANT_ADMIN') {
+ fetchDashboardsPromise = dashboardService.getTenantDashboards(pageLink);
+ } else {
+ fetchDashboardsPromise = dashboardService.getCustomerDashboards(currentUser.customerId, pageLink);
+ }
+ fetchDashboardsPromise.then(
function success(result) {
var dashboards = result.data;
for (var d=0;d<dashboards.length;d++) {
@@ -296,7 +302,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
if (userForceFullscreen()) {
$rootScope.forceFullscreen = true;
}
- if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') {
+ if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' ||
+ currentUser.authority === 'CUSTOMER_USER')) {
fetchAllowedDashboardIds();
} else {
deferred.resolve();
@@ -436,7 +443,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
function forceDefaultPlace(to, params) {
if (currentUser && isAuthenticated()) {
- if (currentUser.authority === 'CUSTOMER_USER') {
+ if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {
if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) {
if (to.name === 'home.profile') {
if (userHasProfile()) {
@@ -458,7 +465,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
function gotoDefaultPlace(params) {
if (currentUser && isAuthenticated()) {
var place = 'home.links';
- if (currentUser.authority === 'CUSTOMER_USER') {
+ if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {
if (userHasDefaultDashboard()) {
place = 'home.dashboards.dashboard';
params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
ui/src/app/app.config.js 2(+1 -1)
diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js
index 7f7e7a5..fef3273 100644
--- a/ui/src/app/app.config.js
+++ b/ui/src/app/app.config.js
@@ -49,7 +49,7 @@ export default function AppConfig($provide,
$translateProvider.useSanitizeValueStrategy('sce');
$translateProvider.preferredLanguage('en_US');
$translateProvider.useLocalStorage();
- $translateProvider.useMissingTranslationHandlerLog();
+ $translateProvider.useMissingTranslationHandler('tbMissingTranslationHandler');
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
addLocaleKorean(locales);
ui/src/app/app.js 10(+10 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 290e9cd..f67be33 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -53,6 +53,7 @@ import thingsboardDialogs from './components/datakey-config-dialog.controller';
import thingsboardMenu from './services/menu.service';
import thingsboardRaf from './common/raf.provider';
import thingsboardUtils from './common/utils.service';
+import thingsboardDashboardUtils from './common/dashboard-utils.service';
import thingsboardTypes from './common/types.constant';
import thingsboardApiTime from './api/time.service';
import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
@@ -62,6 +63,10 @@ import thingsboardHome from './layout';
import thingsboardApiLogin from './api/login.service';
import thingsboardApiDevice from './api/device.service';
import thingsboardApiUser from './api/user.service';
+import thingsboardApiEntityRelation from './api/entity-relation.service';
+import thingsboardApiAsset from './api/asset.service';
+import thingsboardApiAttribute from './api/attribute.service';
+import thingsboardApiEntity from './api/entity.service';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css';
@@ -105,6 +110,7 @@ angular.module('thingsboard', [
thingsboardMenu,
thingsboardRaf,
thingsboardUtils,
+ thingsboardDashboardUtils,
thingsboardTypes,
thingsboardApiTime,
thingsboardKeyboardShortcut,
@@ -114,6 +120,10 @@ angular.module('thingsboard', [
thingsboardApiLogin,
thingsboardApiDevice,
thingsboardApiUser,
+ thingsboardApiEntityRelation,
+ thingsboardApiAsset,
+ thingsboardApiAttribute,
+ thingsboardApiEntity,
uiRouter])
.config(AppConfig)
.factory('globalInterceptor', GlobalInterceptor)
ui/src/app/asset/add-asset.tpl.html 45(+45 -0)
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 0000000..ce22e4e
--- /dev/null
+++ b/ui/src/app/asset/add-asset.tpl.html
@@ -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.
+
+-->
+<md-dialog aria-label="{{ 'asset.add' | translate }}" tb-help="'assets'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>asset.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-asset asset="vm.item" is-edit="true" the-form="theForm"></tb-asset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
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 0000000..53feb7d
--- /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 0000000..18e23ce
--- /dev/null
+++ b/ui/src/app/asset/add-assets-to-customer.tpl.html
@@ -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.
+
+-->
+<md-dialog aria-label="{{ 'asset.assign-to-customer' | translate }}">
+ <form name="theForm" ng-submit="vm.assign()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>asset.assign-asset-to-customer</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset>
+ <span translate>asset.assign-asset-to-customer-text</span>
+ <md-input-container class="md-block" style='margin-bottom: 0px;'>
+ <label> </label>
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+ search
+ </md-icon>
+ <input id="asset-search" autofocus ng-model="vm.searchText"
+ ng-change="vm.searchAssetTextUpdated()"
+ placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <div style='min-height: 150px;'>
+ <span translate layout-align="center center"
+ style="text-transform: uppercase; display: flex; height: 150px;"
+ class="md-subhead"
+ ng-show="vm.noData()">asset.no-assets-text</span>
+ <md-virtual-repeat-container ng-show="vm.hasData()"
+ tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+ style='min-height: 150px; width: 100%;'>
+ <md-list>
+ <md-list-item md-virtual-repeat="asset in vm.theAssets" md-on-demand
+ class="repeated-item" flex>
+ <md-checkbox ng-click="vm.toggleAssetSelection($event, asset)"
+ aria-label="{{ 'item.selected' | translate }}"
+ ng-checked="asset.selected"></md-checkbox>
+ <span> {{ asset.name }} </span>
+ </md-list-item>
+ </md-list>
+ </md-virtual-repeat-container>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || vm.assets.selectedCount == 0" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.assign' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
ui/src/app/asset/asset.controller.js 506(+506 -0)
diff --git a/ui/src/app/asset/asset.controller.js b/ui/src/app/asset/asset.controller.js
new file mode 100644
index 0000000..d0944ae
--- /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();
+ });
+ });
+ }
+}
ui/src/app/asset/asset.directive.js 71(+71 -0)
diff --git a/ui/src/app/asset/asset.directive.js b/ui/src/app/asset/asset.directive.js
new file mode 100644
index 0000000..8c13082
--- /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: '&'
+ }
+ };
+}
ui/src/app/asset/asset.routes.js 68(+68 -0)
diff --git a/ui/src/app/asset/asset.routes.js b/ui/src/app/asset/asset.routes.js
new file mode 100644
index 0000000..c9a312d
--- /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"}'
+ }
+ });
+
+}
ui/src/app/asset/asset-card.tpl.html 19(+19 -0)
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 0000000..3c06558
--- /dev/null
+++ b/ui/src/app/asset/asset-card.tpl.html
@@ -0,0 +1,19 @@
+<!--
+
+ 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.
+
+-->
+<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+<div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
ui/src/app/asset/asset-fieldset.tpl.html 71(+71 -0)
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 0000000..8cf0c96
--- /dev/null
+++ b/ui/src/app/asset/asset-fieldset.tpl.html
@@ -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.
+
+-->
+<md-button ng-click="onMakePublic({event: $event})"
+ ng-show="!isEdit && assetScope === 'tenant' && !isAssignedToCustomer && !isPublic"
+ class="md-raised md-primary">{{ 'asset.make-public' | translate }}</md-button>
+<md-button ng-click="onAssignToCustomer({event: $event})"
+ ng-show="!isEdit && assetScope === 'tenant' && !isAssignedToCustomer"
+ class="md-raised md-primary">{{ 'asset.assign-to-customer' | translate }}</md-button>
+<md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
+ ng-show="!isEdit && (assetScope === 'customer' || assetScope === 'tenant') && isAssignedToCustomer"
+ class="md-raised md-primary">{{ isPublic ? 'asset.make-private' : 'asset.unassign-from-customer' | translate }}</md-button>
+<md-button ng-click="onDeleteAsset({event: $event})"
+ ng-show="!isEdit && assetScope === 'tenant'"
+ class="md-raised md-primary">{{ 'asset.delete' | translate }}</md-button>
+
+<div layout="row">
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onAssetIdCopied(e)"
+ data-clipboard-text="{{asset.id.id}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>asset.copyId</span>
+ </md-button>
+</div>
+
+<md-content class="md-padding" layout="column">
+ <md-input-container class="md-block"
+ ng-show="!isEdit && isAssignedToCustomer && !isPublic && assetScope === 'tenant'">
+ <label translate>asset.assignedToCustomer</label>
+ <input ng-model="assignedCustomer.title" disabled>
+ </md-input-container>
+ <div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
+ ng-show="!isEdit && isPublic && (assetScope === 'customer' || assetScope === 'tenant')">
+ {{ 'asset.asset-public' | translate }}
+ </div>
+ <fieldset ng-disabled="loading || !isEdit">
+ <md-input-container class="md-block">
+ <label translate>asset.name</label>
+ <input required name="name" ng-model="asset.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">asset.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>asset.type</label>
+ <input required name="type" ng-model="asset.type">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">asset.type-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>asset.description</label>
+ <textarea ng-model="asset.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/asset/assets.tpl.html 58(+58 -0)
diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html
new file mode 100644
index 0000000..fb3cf56
--- /dev/null
+++ b/ui/src/app/asset/assets.tpl.html
@@ -0,0 +1,58 @@
+<!--
+
+ 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-grid grid-configuration="vm.assetGridConfig">
+ <details-buttons tb-help="'assets'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'asset.details' | translate }}">
+ <tb-asset asset="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ asset-scope="vm.assetsScope"
+ the-form="vm.grid.detailsForm"
+ on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
+ on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
+ on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
+ on-delete-asset="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-asset>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.asset}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.asset}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+ disable-attribute-scope-selection="true">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'asset.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.asset"
+ entity-id="vm.grid.operatingItem().id.id"
+ tenant-id="vm.grid.operatingItem().tenantId.id"
+ default-event-type="{{vm.types.eventType.alarm.value}}">
+ </tb-event-table>
+ </md-tab>
+</tb-grid>
ui/src/app/asset/assign-to-customer.controller.js 123(+123 -0)
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 0000000..602e599
--- /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
+ };
+ }
+}
ui/src/app/asset/assign-to-customer.tpl.html 76(+76 -0)
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 0000000..fba56ce
--- /dev/null
+++ b/ui/src/app/asset/assign-to-customer.tpl.html
@@ -0,0 +1,76 @@
+<!--
+
+ 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-dialog aria-label="{{ 'asset.assign-asset-to-customer' | translate }}">
+ <form name="theForm" ng-submit="vm.assign()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>asset.assign-asset-to-customer</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset>
+ <span translate>asset.assign-to-customer-text</span>
+ <md-input-container class="md-block" style='margin-bottom: 0px;'>
+ <label> </label>
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+ search
+ </md-icon>
+ <input id="customer-search" autofocus ng-model="vm.searchText"
+ ng-change="vm.searchCustomerTextUpdated()"
+ placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <div style='min-height: 150px;'>
+ <span translate layout-align="center center"
+ style="text-transform: uppercase; display: flex; height: 150px;"
+ class="md-subhead"
+ ng-show="vm.noData()">customer.no-customers-text</span>
+ <md-virtual-repeat-container ng-show="vm.hasData()"
+ tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+ style='min-height: 150px; width: 100%;'>
+ <md-list>
+ <md-list-item md-virtual-repeat="customer in vm.theCustomers" md-on-demand
+ class="repeated-item" flex>
+ <md-checkbox ng-click="vm.toggleCustomerSelection($event, customer)"
+ aria-label="{{ 'item.selected' | translate }}"
+ ng-checked="vm.isCustomerSelected(customer)"></md-checkbox>
+ <span> {{ customer.title }} </span>
+ </md-list-item>
+ </md-list>
+ </md-virtual-repeat-container>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || vm.customers.selection==null" type="submit" class="md-raised md-primary">
+ {{ 'action.assign' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
ui/src/app/asset/index.js 43(+43 -0)
diff --git a/ui/src/app/asset/index.js b/ui/src/app/asset/index.js
new file mode 100644
index 0000000..62ab201
--- /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;
ui/src/app/common/dashboard-utils.service.js 437(+437 -0)
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 0000000..57317b5
--- /dev/null
+++ b/ui/src/app/common/dashboard-utils.service.js
@@ -0,0 +1,437 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default angular.module('thingsboard.dashboardUtils', [])
+ .factory('dashboardUtils', DashboardUtils)
+ .name;
+
+/*@ngInject*/
+function DashboardUtils(types, utils, timeService) {
+
+ var service = {
+ validateAndUpdateDashboard: validateAndUpdateDashboard,
+ getRootStateId: getRootStateId,
+ createSingleWidgetDashboard: createSingleWidgetDashboard,
+ getStateLayoutsData: getStateLayoutsData,
+ createDefaultState: createDefaultState,
+ createDefaultLayoutData: createDefaultLayoutData,
+ setLayouts: setLayouts,
+ updateLayoutSettings: updateLayoutSettings,
+ addWidgetToLayout: addWidgetToLayout,
+ removeWidgetFromLayout: removeWidgetFromLayout,
+ isSingleLayoutDashboard: isSingleLayoutDashboard,
+ removeUnusedWidgets: removeUnusedWidgets,
+ getWidgetsArray: getWidgetsArray
+ };
+
+ return service;
+
+ function validateAndUpdateEntityAliases(configuration) {
+ if (angular.isUndefined(configuration.entityAliases)) {
+ configuration.entityAliases = {};
+ if (configuration.deviceAliases) {
+ var deviceAliases = configuration.deviceAliases;
+ for (var aliasId in deviceAliases) {
+ var deviceAlias = deviceAliases[aliasId];
+ var alias = deviceAlias.alias;
+ var entityFilter = {
+ useFilter: false,
+ entityNameFilter: '',
+ entityList: []
+ }
+ if (deviceAlias.deviceFilter) {
+ entityFilter.useFilter = deviceAlias.deviceFilter.useFilter;
+ entityFilter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter;
+ entityFilter.entityList = deviceAlias.deviceFilter.deviceList;
+ } else if (deviceAlias.deviceId) {
+ entityFilter.entityList = [deviceAlias.deviceId];
+ }
+ var entityAlias = {
+ id: aliasId,
+ alias: alias,
+ entityType: types.entityType.device,
+ entityFilter: entityFilter
+ };
+ configuration.entityAliases[aliasId] = entityAlias;
+ }
+ delete configuration.deviceAliases;
+ }
+ }
+ return configuration;
+ }
+
+ function validateAndUpdateWidget(widget) {
+ if (!widget.config) {
+ widget.config = {};
+ }
+ if (!widget.config.datasources) {
+ widget.config.datasources = [];
+ }
+ widget.config.datasources.forEach(function(datasource) {
+ if (datasource.type === 'device') {
+ datasource.type = types.datasourceType.entity;
+ }
+ if (datasource.deviceAliasId) {
+ datasource.entityAliasId = datasource.deviceAliasId;
+ delete datasource.deviceAliasId;
+ }
+ });
+ return widget;
+ }
+
+ function createDefaultLayoutData() {
+ return {
+ widgets: {},
+ gridSettings: {
+ backgroundColor: '#eeeeee',
+ color: 'rgba(0,0,0,0.870588)',
+ columns: 24,
+ margins: [10, 10],
+ backgroundSizeMode: '100%'
+ }
+ };
+ }
+
+ function createDefaultLayouts() {
+ return {
+ 'main': createDefaultLayoutData()
+ };
+ }
+
+ function createDefaultState(name, root) {
+ return {
+ name: name,
+ root: root,
+ layouts: createDefaultLayouts()
+ }
+ }
+
+ function validateAndUpdateDashboard(dashboard) {
+ if (!dashboard.configuration) {
+ dashboard.configuration = {};
+ }
+ if (angular.isUndefined(dashboard.configuration.widgets)) {
+ dashboard.configuration.widgets = {};
+ } else if (angular.isArray(dashboard.configuration.widgets)) {
+ var widgetsMap = {};
+ dashboard.configuration.widgets.forEach(function (widget) {
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ widgetsMap[widget.id] = validateAndUpdateWidget(widget);
+ });
+ dashboard.configuration.widgets = widgetsMap;
+ }
+ if (angular.isUndefined(dashboard.configuration.states)) {
+ dashboard.configuration.states = {
+ 'default': createDefaultState('Default', true)
+ };
+
+ var mainLayout = dashboard.configuration.states['default'].layouts['main'];
+ for (var id in dashboard.configuration.widgets) {
+ var widget = dashboard.configuration.widgets[id];
+ mainLayout.widgets[id] = {
+ sizeX: widget.sizeX,
+ sizeY: widget.sizeY,
+ row: widget.row,
+ col: widget.col,
+ };
+ }
+ } else {
+ var states = dashboard.configuration.states;
+ var rootFound = false;
+ for (var stateId in states) {
+ var state = states[stateId];
+ if (angular.isUndefined(state.root)) {
+ state.root = false;
+ } else if (state.root) {
+ rootFound = true;
+ }
+ }
+ if (!rootFound) {
+ var firstStateId = Object.keys(states)[0];
+ states[firstStateId].root = true;
+ }
+ }
+ dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration);
+
+ if (angular.isUndefined(dashboard.configuration.timewindow)) {
+ dashboard.configuration.timewindow = timeService.defaultTimewindow();
+ }
+ if (angular.isUndefined(dashboard.configuration.settings)) {
+ dashboard.configuration.settings = {};
+ dashboard.configuration.settings.stateControllerId = 'default';
+ dashboard.configuration.settings.showTitle = true;
+ dashboard.configuration.settings.showDashboardsSelect = true;
+ dashboard.configuration.settings.showEntitiesSelect = true;
+ dashboard.configuration.settings.showDashboardTimewindow = true;
+ dashboard.configuration.settings.showDashboardExport = true;
+ } else {
+ if (angular.isUndefined(dashboard.configuration.settings.stateControllerId)) {
+ dashboard.configuration.settings.stateControllerId = 'default';
+ }
+ }
+ if (angular.isDefined(dashboard.configuration.gridSettings)) {
+ var gridSettings = dashboard.configuration.gridSettings;
+ if (angular.isDefined(gridSettings.showTitle)) {
+ dashboard.configuration.settings.showTitle = gridSettings.showTitle;
+ delete gridSettings.showTitle;
+ }
+ if (angular.isDefined(gridSettings.titleColor)) {
+ dashboard.configuration.settings.titleColor = gridSettings.titleColor;
+ delete gridSettings.titleColor;
+ }
+ if (angular.isDefined(gridSettings.showDevicesSelect)) {
+ dashboard.configuration.settings.showEntitiesSelect = gridSettings.showDevicesSelect;
+ delete gridSettings.showDevicesSelect;
+ }
+ if (angular.isDefined(gridSettings.showEntitiesSelect)) {
+ dashboard.configuration.settings.showEntitiesSelect = gridSettings.showEntitiesSelect;
+ delete gridSettings.showEntitiesSelect;
+ }
+ if (angular.isDefined(gridSettings.showDashboardTimewindow)) {
+ dashboard.configuration.settings.showDashboardTimewindow = gridSettings.showDashboardTimewindow;
+ delete gridSettings.showDashboardTimewindow;
+ }
+ if (angular.isDefined(gridSettings.showDashboardExport)) {
+ dashboard.configuration.settings.showDashboardExport = gridSettings.showDashboardExport;
+ delete gridSettings.showDashboardExport;
+ }
+ dashboard.configuration.states['default'].layouts['main'].gridSettings = gridSettings;
+ delete dashboard.configuration.gridSettings;
+ }
+ return dashboard;
+ }
+
+ function getRootStateId(states) {
+ for (var stateId in states) {
+ var state = states[stateId];
+ if (state.root) {
+ return stateId;
+ }
+ }
+ return Object.keys(states)[0];
+ }
+
+ function createSingleWidgetDashboard(widget) {
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ var dashboard = {};
+ dashboard = validateAndUpdateDashboard(dashboard);
+ dashboard.configuration.widgets[widget.id] = widget;
+ dashboard.configuration.states['default'].layouts['main'].widgets[widget.id] = {
+ sizeX: widget.sizeX,
+ sizeY: widget.sizeY,
+ row: widget.row,
+ col: widget.col,
+ };
+ return dashboard;
+ }
+
+ function getStateLayoutsData(dashboard, targetState) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var state = states[targetState];
+ if (state) {
+ var allWidgets = dashboardConfiguration.widgets;
+ var result = {};
+ for (var l in state.layouts) {
+ var layout = state.layouts[l];
+ if (layout) {
+ result[l] = {
+ widgets: [],
+ widgetLayouts: {},
+ gridSettings: {}
+ }
+ for (var id in layout.widgets) {
+ result[l].widgets.push(allWidgets[id]);
+ }
+ result[l].widgetLayouts = layout.widgets;
+ result[l].gridSettings = layout.gridSettings;
+ }
+ }
+ return result;
+ } else {
+ return null;
+ }
+ }
+
+ function setLayouts(dashboard, targetState, newLayouts) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var state = states[targetState];
+ var addedCount = 0;
+ var removedCount = 0;
+ for (var l in state.layouts) {
+ if (!newLayouts[l]) {
+ removedCount++;
+ }
+ }
+ for (l in newLayouts) {
+ if (!state.layouts[l]) {
+ addedCount++;
+ }
+ }
+ state.layouts = newLayouts;
+ var layoutsCount = Object.keys(state.layouts).length;
+ var newColumns;
+ if (addedCount) {
+ for (l in state.layouts) {
+ newColumns = state.layouts[l].gridSettings.columns * (layoutsCount - addedCount) / layoutsCount;
+ state.layouts[l].gridSettings.columns = newColumns;
+ }
+ }
+ if (removedCount) {
+ for (l in state.layouts) {
+ newColumns = state.layouts[l].gridSettings.columns * (layoutsCount + removedCount) / layoutsCount;
+ state.layouts[l].gridSettings.columns = newColumns;
+ }
+ }
+ removeUnusedWidgets(dashboard);
+ }
+
+ function updateLayoutSettings(layout, gridSettings) {
+ var prevGridSettings = layout.gridSettings;
+ var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
+ var ratio = gridSettings.columns / prevColumns;
+ layout.gridSettings = gridSettings;
+ for (var w in layout.widgets) {
+ var widget = layout.widgets[w];
+ widget.sizeX = Math.round(widget.sizeX * ratio);
+ widget.sizeY = Math.round(widget.sizeY * ratio);
+ widget.col = Math.round(widget.col * ratio);
+ widget.row = Math.round(widget.row * ratio);
+ }
+ }
+
+ function addWidgetToLayout(dashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var state = states[targetState];
+ var layout = state.layouts[targetLayout];
+ var layoutCount = Object.keys(state.layouts).length;
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ if (!dashboardConfiguration.widgets[widget.id]) {
+ dashboardConfiguration.widgets[widget.id] = widget;
+ }
+ var widgetLayout = {
+ sizeX: originalSize ? originalSize.sizeX : widget.sizeX,
+ sizeY: originalSize ? originalSize.sizeY : widget.sizeY,
+ mobileOrder: widget.config.mobileOrder,
+ mobileHeight: widget.config.mobileHeight
+ };
+
+ if (angular.isUndefined(originalColumns)) {
+ originalColumns = 24;
+ }
+
+ var gridSettings = layout.gridSettings;
+ var columns = 24;
+ if (gridSettings && gridSettings.columns) {
+ columns = gridSettings.columns;
+ }
+
+ columns = columns * layoutCount;
+
+ if (columns != originalColumns) {
+ var ratio = columns / originalColumns;
+ widgetLayout.sizeX *= ratio;
+ widgetLayout.sizeY *= ratio;
+ }
+
+ if (row > -1 && column > - 1) {
+ widgetLayout.row = row;
+ widgetLayout.col = column;
+ } else {
+ row = 0;
+ for (var w in layout.widgets) {
+ var existingLayout = layout.widgets[w];
+ var wRow = existingLayout.row ? existingLayout.row : 0;
+ var wSizeY = existingLayout.sizeY ? existingLayout.sizeY : 1;
+ var bottom = wRow + wSizeY;
+ row = Math.max(row, bottom);
+ }
+ widgetLayout.row = row;
+ widgetLayout.col = 0;
+ }
+
+ layout.widgets[widget.id] = widgetLayout;
+ }
+
+ function removeWidgetFromLayout(dashboard, targetState, targetLayout, widgetId) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var state = states[targetState];
+ var layout = state.layouts[targetLayout];
+ delete layout.widgets[widgetId];
+ removeUnusedWidgets(dashboard);
+ }
+
+ function isSingleLayoutDashboard(dashboard) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var stateKeys = Object.keys(states);
+ if (stateKeys.length === 1) {
+ var state = states[stateKeys[0]];
+ var layouts = state.layouts;
+ var layoutKeys = Object.keys(layouts);
+ if (layoutKeys.length === 1) {
+ return {
+ state: stateKeys[0],
+ layout: layoutKeys[0]
+ }
+ }
+ }
+ return null;
+ }
+
+ function removeUnusedWidgets(dashboard) {
+ var dashboardConfiguration = dashboard.configuration;
+ var states = dashboardConfiguration.states;
+ var widgets = dashboardConfiguration.widgets;
+ for (var widgetId in widgets) {
+ var found = false;
+ for (var s in states) {
+ var state = states[s];
+ for (var l in state.layouts) {
+ var layout = state.layouts[l];
+ if (layout.widgets[widgetId]) {
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ delete dashboardConfiguration.widgets[widgetId];
+ }
+
+ }
+ }
+
+ function getWidgetsArray(dashboard) {
+ var widgetsArray = [];
+ var dashboardConfiguration = dashboard.configuration;
+ var widgets = dashboardConfiguration.widgets;
+ for (var widgetId in widgets) {
+ var widget = widgets[widgetId];
+ widgetsArray.push(widget);
+ }
+ return widgetsArray;
+ }
+}
ui/src/app/common/types.constant.js 22(+17 -5)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index df321f7..a8d8556 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -79,7 +79,7 @@ export default angular.module('thingsboard.types', [])
},
datasourceType: {
function: "function",
- device: "device"
+ entity: "entity"
},
dataKeyType: {
timeseries: "timeseries",
@@ -93,11 +93,20 @@ export default angular.module('thingsboard.types', [])
plugin: "PLUGIN"
},
entityType: {
- tenant: "TENANT",
device: "DEVICE",
- customer: "CUSTOMER",
+ asset: "ASSET",
rule: "RULE",
- plugin: "PLUGIN"
+ plugin: "PLUGIN",
+ tenant: "TENANT",
+ customer: "CUSTOMER"
+ },
+ entitySearchDirection: {
+ from: "FROM",
+ to: "TO"
+ },
+ entityRelationType: {
+ contains: "Contains",
+ manages: "Manages"
},
eventType: {
alarm: {
@@ -122,7 +131,7 @@ export default angular.module('thingsboard.types', [])
name: "attribute.scope-latest-telemetry",
clientSide: true
},
- deviceAttributesScope: {
+ attributesScope: {
client: {
value: "CLIENT_SCOPE",
name: "attribute.scope-client",
@@ -198,6 +207,9 @@ export default angular.module('thingsboard.types', [])
systemBundleAlias: {
charts: "charts",
cards: "cards"
+ },
+ translate: {
+ dashboardStatePrefix: "dashboardState.state."
}
}
).name;
ui/src/app/common/utils.service.js 162(+34 -128)
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
index 6e05bbb..b324cbf 100644
--- a/ui/src/app/common/utils.service.js
+++ b/ui/src/app/common/utils.service.js
@@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
.name;
/*@ngInject*/
-function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
+function Utils($mdColorPalette, $rootScope, $window, types) {
var predefinedFunctions = {},
predefinedFunctionsList = [],
@@ -106,8 +106,9 @@ function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
filterSearchTextEntities: filterSearchTextEntities,
guid: guid,
- createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo,
- isLocalUrl: isLocalUrl
+ isLocalUrl: isLocalUrl,
+ validateDatasources: validateDatasources,
+ createKey: createKey
}
return service;
@@ -300,21 +301,37 @@ function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
return getMaterialColor(index);
}
- /*var defaultDataKey = {
- name: 'f(x)',
- type: types.dataKeyType.function,
- label: 'Sin',
- color: getMaterialColor(0),
- funcBody: getPredefinedFunctionBody('Sin'),
- settings: {},
- _hash: Math.random()
- };
+ function isLocalUrl(url) {
+ var parser = document.createElement('a'); //eslint-disable-line
+ parser.href = url;
+ var host = parser.hostname;
+ if (host === "localhost" || host === "127.0.0.1") {
+ return true;
+ } else {
+ return false;
+ }
+ }
- var defaultDatasource = {
- type: types.datasourceType.function,
- name: types.datasourceType.function,
- dataKeys: [angular.copy(defaultDataKey)]
- };*/
+ function validateDatasources(datasources) {
+ datasources.forEach(function (datasource) {
+ if (datasource.type === 'device') {
+ datasource.type = types.datasourceType.entity;
+ datasource.entityType = types.entityType.device;
+ if (datasource.deviceId) {
+ datasource.entityId = datasource.deviceId;
+ } else if (datasource.deviceAliasId) {
+ datasource.entityAliasId = datasource.deviceAliasId;
+ }
+ if (datasource.deviceName) {
+ datasource.entityName = datasource.deviceName;
+ }
+ }
+ if (datasource.type === types.datasourceType.entity && datasource.entityId) {
+ datasource.name = datasource.entityName;
+ }
+ });
+ return datasources;
+ }
function createKey(keyInfo, type, datasources) {
var dataKey = {
@@ -329,115 +346,4 @@ function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
return dataKey;
}
- function createDatasourceKeys(keyInfos, type, datasource, datasources) {
- for (var i=0;i<keyInfos.length;i++) {
- var keyInfo = keyInfos[i];
- var dataKey = createKey(keyInfo, type, datasources);
- datasource.dataKeys.push(dataKey);
- }
- }
-
- function createDatasourceFromSubscription(subscriptionInfo, datasources, device) {
- var datasource;
- if (subscriptionInfo.type === types.datasourceType.device) {
- datasource = {
- type: subscriptionInfo.type,
- deviceName: device.name,
- name: device.name,
- deviceId: device.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 processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred) {
- if (index < subscriptionsInfo.length) {
- var subscriptionInfo = subscriptionsInfo[index];
- if (subscriptionInfo.type === types.datasourceType.device) {
- if (subscriptionInfo.deviceId) {
- deviceService.getDevice(subscriptionInfo.deviceId, true, {ignoreLoading: true}).then(
- function success(device) {
- createDatasourceFromSubscription(subscriptionInfo, datasources, device);
- index++;
- processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
- },
- function fail() {
- index++;
- processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
- }
- );
- } else if (subscriptionInfo.deviceName || subscriptionInfo.deviceNamePrefix
- || subscriptionInfo.deviceIds) {
- var promise;
- if (subscriptionInfo.deviceName) {
- promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceName, 1, false, {ignoreLoading: true});
- } else if (subscriptionInfo.deviceNamePrefix) {
- promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceNamePrefix, 100, false, {ignoreLoading: true});
- } else if (subscriptionInfo.deviceIds) {
- promise = deviceService.getDevices(subscriptionInfo.deviceIds, {ignoreLoading: true});
- }
- promise.then(
- function success(devices) {
- if (devices && devices.length > 0) {
- for (var i = 0; i < devices.length; i++) {
- var device = devices[i];
- createDatasourceFromSubscription(subscriptionInfo, datasources, device);
- }
- }
- index++;
- processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
- },
- function fail() {
- index++;
- processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
- }
- )
- } else {
- index++;
- processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
- }
- } else if (subscriptionInfo.type === types.datasourceType.function) {
- createDatasourceFromSubscription(subscriptionInfo, datasources);
- index++;
- processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
- }
- } else {
- deferred.resolve(datasources);
- }
- }
-
- function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) {
- var deferred = $q.defer();
- var datasources = [];
- processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred);
- return deferred.promise;
- }
-
- function isLocalUrl(url) {
- var parser = document.createElement('a'); //eslint-disable-line
- parser.href = url;
- var host = parser.hostname;
- if (host === "localhost" || host === "127.0.0.1") {
- return true;
- } else {
- return false;
- }
- }
-
}
ui/src/app/components/dashboard.directive.js 191(+165 -26)
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index 131616b..f26121c 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -51,7 +51,9 @@ function Dashboard() {
scope: true,
bindToController: {
widgets: '=',
+ widgetLayouts: '=?',
aliasesInfo: '=',
+ stateController: '=',
dashboardTimewindow: '=?',
columns: '=',
margins: '=',
@@ -73,7 +75,8 @@ function Dashboard() {
onInit: '&?',
onInitFailed: '&?',
dashboardStyle: '=?',
- dashboardClass: '=?'
+ dashboardClass: '=?',
+ ignoreLoading: '=?'
},
controller: DashboardController,
controllerAs: 'vm',
@@ -82,7 +85,7 @@ function Dashboard() {
}
/*@ngInject*/
-function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types) {
+function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types, utils) {
var highlightedMode = false;
var highlightedWidget = null;
@@ -132,14 +135,26 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
updateMobileOpts();
+ vm.widgetLayoutInfo = {
+ };
+
vm.widgetItemMap = {
+ sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX',
+ sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY',
+ row: 'vm.widgetLayoutInfo[widget.id].row',
+ col: 'vm.widgetLayoutInfo[widget.id].col',
+ minSizeY: 'widget.minSizeY',
+ maxSizeY: 'widget.maxSizeY'
+ };
+
+ /*vm.widgetItemMap = {
sizeX: 'vm.widgetSizeX(widget)',
sizeY: 'vm.widgetSizeY(widget)',
- row: 'widget.row',
- col: 'widget.col',
+ row: 'vm.widgetRow(widget)',
+ col: 'vm.widgetCol(widget)',
minSizeY: 'widget.minSizeY',
maxSizeY: 'widget.maxSizeY'
- };
+ };*/
vm.isWidgetExpanded = false;
vm.isHighlighted = isHighlighted;
@@ -156,6 +171,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
vm.widgetSizeX = widgetSizeX;
vm.widgetSizeY = widgetSizeY;
+ vm.widgetRow = widgetRow;
+ vm.widgetCol = widgetCol;
vm.widgetColor = widgetColor;
vm.widgetBackgroundColor = widgetBackgroundColor;
vm.widgetPadding = widgetPadding;
@@ -173,6 +190,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
vm.openWidgetContextMenu = openWidgetContextMenu;
vm.getEventGridPosition = getEventGridPosition;
+ vm.reload = reload;
vm.contextMenuItems = [];
vm.contextMenuEvent = null;
@@ -199,6 +217,45 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
};
+ $scope.$watchCollection('vm.widgets', function () {
+ var ids = [];
+ for (var i=0;i<vm.widgets.length;i++) {
+ var widget = vm.widgets[i];
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ ids.push(widget.id);
+ var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
+ if (!layoutInfoObject) {
+ layoutInfoObject = {
+ widget: widget
+ };
+ Object.defineProperty(layoutInfoObject, 'sizeX', {
+ get: function() { return widgetSizeX(this.widget) },
+ set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
+ });
+ Object.defineProperty(layoutInfoObject, 'sizeY', {
+ get: function() { return widgetSizeY(this.widget) },
+ set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
+ });
+ Object.defineProperty(layoutInfoObject, 'row', {
+ get: function() { return widgetRow(this.widget) },
+ set: function(newRow) { setWidgetRow(this.widget, newRow)}
+ });
+ Object.defineProperty(layoutInfoObject, 'col', {
+ get: function() { return widgetCol(this.widget) },
+ set: function(newCol) { setWidgetCol(this.widget, newCol)}
+ });
+ vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
+ }
+ }
+ for (var widgetId in vm.widgetLayoutInfo) {
+ if (ids.indexOf(widgetId) === -1) {
+ delete vm.widgetLayoutInfo[widgetId];
+ }
+ }
+ });
+
//TODO: widgets visibility
/*gridsterParent.scroll(function () {
updateVisibleRect();
@@ -272,13 +329,14 @@ 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) {
if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster;
+ vm.isResizing = false;
//TODO: widgets visibility
//updateVisibleRect(false, true);
}
@@ -302,20 +360,22 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
});
+ function widgetOrder(widget) {
+ var order;
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ order = vm.widgetLayouts[widget.id].mobileOrder;
+ } else if (widget.config.mobileOrder) {
+ order = widget.config.mobileOrder;
+ } else {
+ order = widget.row;
+ }
+ return order;
+ }
+
$scope.$on('widgetPositionChanged', function () {
vm.widgets.sort(function (widget1, widget2) {
- var row1;
- var row2;
- if (angular.isDefined(widget1.config.mobileOrder)) {
- row1 = widget1.config.mobileOrder;
- } else {
- row1 = widget1.row;
- }
- if (angular.isDefined(widget2.config.mobileOrder)) {
- row2 = widget2.config.mobileOrder;
- } else {
- row2 = widget2.row;
- }
+ var row1 = widgetOrder(widget1);
+ var row2 = widgetOrder(widget2);
var res = row1 - row2;
if (res === 0) {
res = widget1.col - widget2.col;
@@ -326,6 +386,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
loadStDiff();
+ function reload() {
+ loadStDiff();
+ }
+
function loadStDiff() {
if (vm.getStDiff) {
var promise = vm.getStDiff();
@@ -568,18 +632,89 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function widgetSizeX(widget) {
- return widget.sizeX;
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ return vm.widgetLayouts[widget.id].sizeX;
+ } else {
+ return widget.sizeX;
+ }
+ }
+
+ function setWidgetSizeX(widget, sizeX) {
+ if (!vm.gridsterOpts.isMobile) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ vm.widgetLayouts[widget.id].sizeX = sizeX;
+ } else {
+ widget.sizeX = sizeX;
+ }
+ }
}
function widgetSizeY(widget) {
if (vm.gridsterOpts.isMobile) {
- if (widget.config.mobileHeight) {
- return widget.config.mobileHeight;
+ var mobileHeight;
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ mobileHeight = vm.widgetLayouts[widget.id].mobileHeight;
+ }
+ if (!mobileHeight && widget.config.mobileHeight) {
+ mobileHeight = widget.config.mobileHeight;
+ }
+ if (mobileHeight) {
+ return mobileHeight;
} else {
return widget.sizeY * 24 / vm.gridsterOpts.columns;
}
} else {
- return widget.sizeY;
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ return vm.widgetLayouts[widget.id].sizeY;
+ } else {
+ return widget.sizeY;
+ }
+ }
+ }
+
+ function setWidgetSizeY(widget, sizeY) {
+ if (!vm.gridsterOpts.isMobile) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ vm.widgetLayouts[widget.id].sizeY = sizeY;
+ } else {
+ widget.sizeY = sizeY;
+ }
+ }
+ }
+
+ function widgetRow(widget) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ return vm.widgetLayouts[widget.id].row;
+ } else {
+ return widget.row;
+ }
+ }
+
+ function setWidgetRow(widget, row) {
+ if (!vm.gridsterOpts.isMobile) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ vm.widgetLayouts[widget.id].row = row;
+ } else {
+ widget.row = row;
+ }
+ }
+ }
+
+ function widgetCol(widget) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ return vm.widgetLayouts[widget.id].col;
+ } else {
+ return widget.col;
+ }
+ }
+
+ function setWidgetCol(widget, col) {
+ if (!vm.gridsterOpts.isMobile) {
+ if (vm.widgetLayouts && vm.widgetLayouts[widget.id]) {
+ vm.widgetLayouts[widget.id].col = col;
+ } else {
+ widget.col = col;
+ }
}
}
@@ -653,7 +788,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
var maxRows = vm.gridsterOpts.maxRows;
for (var i = 0; i < vm.widgets.length; i++) {
var w = vm.widgets[i];
- var bottom = w.row + w.sizeY;
+ var bottom = widgetRow(w) + widgetSizeY(w);
maxRows = Math.max(maxRows, bottom);
}
vm.gridsterOpts.maxRows = Math.max(maxRows, vm.gridsterOpts.maxRows);
@@ -662,7 +797,11 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
function dashboardLoaded() {
$timeout(function () {
- $scope.$watch('vm.dashboardTimewindow', function () {
+ if (vm.dashboardTimewindowWatch) {
+ vm.dashboardTimewindowWatch();
+ vm.dashboardTimewindowWatch = null;
+ }
+ vm.dashboardTimewindowWatch = $scope.$watch('vm.dashboardTimewindow', function () {
$scope.$broadcast('dashboardTimewindowChanged', vm.dashboardTimewindow);
}, true);
adoptMaxRows();
@@ -678,7 +817,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function loading() {
- return $rootScope.loading;
+ return !vm.ignoreLoading && $rootScope.loading;
}
}
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 3d11e46..69934ac 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -16,8 +16,10 @@
-->
<md-content flex layout="column" class="tb-progress-cover" layout-align="center center"
- ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit">
- <md-progress-circular md-mode="indeterminate" ng-disabled="!vm.loading() && !vm.dashboardLoading || vm.isEdit" class="md-warn" md-diameter="100"></md-progress-circular>
+ ng-style="vm.dashboardStyle"
+ ng-show="((vm.loading() || vm.dashboardLoading) && !vm.isEdit) || vm.isResizing">
+ <md-progress-circular md-mode="indeterminate" ng-disabled="(!vm.loading() && !vm.dashboardLoading || vm.isEdit) && !vm.isResizing" class="md-warn" md-diameter="100">
+ </md-progress-circular>
</md-content>
<md-menu md-position-mode="target target" tb-mousepoint-menu>
<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
@@ -88,6 +90,7 @@
locals="{ visibleRect: vm.visibleRect,
widget: widget,
aliasesInfo: vm.aliasesInfo,
+ stateController: vm.stateController,
isEdit: vm.isEdit,
stDiff: vm.stDiff,
dashboardTimewindow: vm.dashboardTimewindow,
diff --git a/ui/src/app/components/dashboard-autocomplete.directive.js b/ui/src/app/components/dashboard-autocomplete.directive.js
index afa5f56..77e3c1c 100644
--- a/ui/src/app/components/dashboard-autocomplete.directive.js
+++ b/ui/src/app/components/dashboard-autocomplete.directive.js
@@ -53,7 +53,15 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
promise = $q.when({data: []});
}
} else {
- promise = dashboardService.getTenantDashboards(pageLink);
+ if (userService.getAuthority() === 'SYS_ADMIN') {
+ if (scope.tenantId) {
+ promise = dashboardService.getTenantDashboardsByTenantId(scope.tenantId, pageLink);
+ } else {
+ promise = $q.when({data: []});
+ }
+ } else {
+ promise = dashboardService.getTenantDashboards(pageLink);
+ }
}
promise.then(function success(result) {
@@ -76,7 +84,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
- dashboardService.getDashboard(ngModelCtrl.$viewValue).then(
+ dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then(
function success(dashboard) {
scope.dashboard = dashboard;
},
@@ -117,6 +125,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
link: linker,
scope: {
dashboardsScope: '@',
+ tenantId: '=',
customerId: '=',
theForm: '=?',
tbRequired: '=?',
diff --git a/ui/src/app/components/datakey-config.directive.js b/ui/src/app/components/datakey-config.directive.js
index 50bbfcb..9b3081e 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/datakey-config-dialog.controller.js b/ui/src/app/components/datakey-config-dialog.controller.js
index 9bdaae2..a8741f5 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 e827238..a55c316 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 @@
<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content>
<tb-datakey-config ng-model="vm.dataKey"
- fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
- device-alias="vm.deviceAlias"
+ fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)"
+ entity-alias="vm.entityAlias"
datakey-settings-schema="vm.dataKeySettingsSchema">
</tb-datakey-config>
</md-dialog-content>
diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js
index 82e8cdd..2c06a38 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 02bbbff..f188a04 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 cbd638c..88ff247 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})">
</tb-datasource-func>
- <tb-datasource-device flex
+ <tb-datasource-entity flex
ng-model="model"
datakey-settings-schema="datakeySettingsSchema"
- ng-switch-when="device"
- ng-required="model.type === types.datasourceType.device"
+ ng-switch-when="entity"
+ ng-required="model.type === types.datasourceType.entity"
widget-type="widgetType"
- device-aliases="deviceAliases"
+ entity-aliases="entityAliases"
generate-data-key="generateDataKey({chip: chip, type: type})"
- fetch-device-keys="fetchDeviceKeys({deviceAliasId: deviceAliasId, query: query, type: type})"
- on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
- </tb-datasource-device>
+ fetch-entity-keys="fetchEntityKeys({entityAliasId: entityAliasId, query: query, type: type})"
+ on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})">
+ </tb-datasource-entity>
</section>
</section>
ui/src/app/components/datasource-entity.directive.js 249(+249 -0)
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 0000000..97732b9
--- /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 */
ui/src/app/components/datasource-entity.scss 44(+44 -0)
diff --git a/ui/src/app/components/datasource-entity.scss b/ui/src/app/components/datasource-entity.scss
new file mode 100644
index 0000000..7a87fc7
--- /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
ui/src/app/components/datasource-entity.tpl.html 137(+137 -0)
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 0000000..97ee1b5
--- /dev/null
+++ b/ui/src/app/components/datasource-entity.tpl.html
@@ -0,0 +1,137 @@
+<!--
+
+ 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.
+
+-->
+<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
+ <tb-entity-alias-select
+ tb-required="true"
+ entity-aliases="entityAliases"
+ ng-model="entityAlias"
+ on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})">
+ </tb-entity-alias-select>
+ <section flex layout='column'>
+ <section flex layout='column' layout-align="center" style="padding-left: 4px;">
+ <md-chips flex
+ id="timeseries_datakey_chips"
+ ng-required="true"
+ ng-model="timeseriesDataKeys" md-autocomplete-snap
+ md-transform-chip="transformTimeseriesDataKeyChip($chip)"
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="true"
+ id="timeseries_datakey"
+ md-selected-item="selectedTimeseriesDataKey"
+ md-search-text="timeseriesDataKeySearchText"
+ md-items="item in dataKeysSearch(timeseriesDataKeySearchText, types.dataKeyType.timeseries)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{'datakey.timeseries' | translate }}"
+ md-menu-class="tb-timeseries-datakey-autocomplete">
+ <span md-highlight-text="timeseriesDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(timeseriesDataKeySearchText)">
+ <span translate>entity.no-keys-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(timeseriesDataKeySearchText)">
+ <span translate translate-values='{ key: "{{timeseriesDataKeySearchText | truncate:true:6:'...'}}" }'>entity.no-key-matching</span>
+ <span>
+ <a translate ng-click="createKey($event, '#timeseries_datakey_chips')">entity.create-new-key</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <div layout="row" layout-align="start center" class="tb-attribute-chip">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div layout="row" flex>
+ <div class="tb-chip-label">
+ {{$chip.label}}
+ </div>
+ <div class="tb-chip-separator">: </div>
+ <div class="tb-chip-label">
+ <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+ <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ </div>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
+ </md-chip-template>
+ </md-chips>
+ <md-chips flex ng-if="widgetType === types.widgetType.latest.value"
+ id="attribute_datakey_chips"
+ ng-required="true"
+ ng-model="attributeDataKeys" md-autocomplete-snap
+ md-transform-chip="transformAttributeDataKeyChip($chip)"
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="true"
+ id="attribute_datakey"
+ md-selected-item="selectedAttributeDataKey"
+ md-search-text="attributeDataKeySearchText"
+ md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{'datakey.attributes' | translate }}"
+ md-menu-class="tb-attribute-datakey-autocomplete">
+ <span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(attributeDataKeySearchText)">
+ <span translate>entity.no-keys-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(attributeDataKeySearchText)">
+ <span translate translate-values='{ key: "{{attributeDataKeySearchText | truncate:true:6:'...'}}" }'>entity.no-key-matching</span>
+ <span>
+ <a translate ng-click="createKey($event, '#attribute_datakey_chips')">entity.create-new-key</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <div layout="row" layout-align="start center" class="tb-attribute-chip">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div layout="row" flex>
+ <div class="tb-chip-label">
+ {{$chip.label}}
+ </div>
+ <div class="tb-chip-separator">: </div>
+ <div class="tb-chip-label">
+ <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+ <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ </div>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
+ </md-chip-template>
+ </md-chips>
+ </section>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+ <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div>
+ <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
+ </div>
+ </section>
+</section>
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js
index 5d3c2e7..0f66dca 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/entity-alias-select.directive.js b/ui/src/app/components/entity-alias-select.directive.js
new file mode 100644
index 0000000..204b83f
--- /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 0000000..c23982f
--- /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 0000000..37ca52a
--- /dev/null
+++ b/ui/src/app/components/entity-alias-select.tpl.html
@@ -0,0 +1,53 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section layout='column'>
+ <md-autocomplete id="entity-autocomplete"
+ md-input-name="entity_alias"
+ ng-required="tbRequired"
+ md-no-cache="true"
+ ng-model="entityAlias"
+ md-selected-item="entityAlias"
+ md-search-text="entityAliasSearchText"
+ md-items="item in entityAliasSearch(entityAliasSearchText)"
+ md-item-text="item.alias"
+ tb-keydown="entityAliasEnter($event)"
+ tb-keypress="entityAliasEnter($event)"
+ md-min-length="0"
+ placeholder="{{ 'entity.entity-alias' | translate }}"
+ md-menu-class="tb-entity-alias-autocomplete">
+ <md-item-template>
+ <span md-highlight-text="entityAliasSearchText" md-highlight-flags="^i">{{item.alias}}</span>
+ </md-item-template>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(entityAliasSearchText)">
+ <span translate>entity.no-aliases-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(entityAliasSearchText)">
+ <span translate translate-values='{ alias: "{{entityAliasSearchText | truncate:true:6:'...'}}" }'>entity.no-alias-matching</span>
+ <span>
+ <a translate ng-click="createEntityAlias($event, entityAliasSearchText)">entity.create-new-alias</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+ <div translate ng-message="entityAlias" class="tb-error-message">entity.alias-required</div>
+ </div>
+</section>
diff --git a/ui/src/app/components/related-entity-autocomplete.directive.js b/ui/src/app/components/related-entity-autocomplete.directive.js
new file mode 100644
index 0000000..f93ded2
--- /dev/null
+++ b/ui/src/app/components/related-entity-autocomplete.directive.js
@@ -0,0 +1,128 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './related-entity-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import relatedEntityAutocompleteTemplate from './related-entity-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+export default angular.module('thingsboard.directives.relatedEntityAutocomplete', [])
+ .directive('tbRelatedEntityAutocomplete', RelatedEntityAutocomplete)
+ .name;
+
+/*@ngInject*/
+function RelatedEntityAutocomplete($compile, $templateCache, $q, $filter, entityService) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(relatedEntityAutocompleteTemplate);
+ element.html(template);
+
+ scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+ scope.entity = null;
+ scope.entitySearchText = '';
+
+ scope.allEntities = null;
+
+ scope.fetchEntities = function(searchText) {
+ var deferred = $q.defer();
+ if (!scope.allEntities) {
+ entityService.getRelatedEntities(scope.rootEntityId, scope.entityType, scope.entitySubtypes, -1, []).then(
+ function success(entities) {
+ if (scope.excludeEntityId) {
+ var result = $filter('filter')(entities, {id: {id: scope.excludeEntityId.id} }, true);
+ result = $filter('filter')(result, {id: {entityType: scope.excludeEntityId.entityType} }, true);
+ if (result && result.length) {
+ var excludeEntity = result[0];
+ var index = entities.indexOf(excludeEntity);
+ if (index > -1) {
+ entities.splice(index, 1);
+ }
+ }
+ }
+ scope.allEntities = entities;
+ filterEntities(searchText, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ filterEntities(searchText, deferred);
+ }
+ return deferred.promise;
+ }
+
+ function filterEntities(searchText, deferred) {
+ var result = $filter('filter')(scope.allEntities, {name: searchText});
+ deferred.resolve(result);
+ }
+
+ scope.entitySearchTextChanged = function() {
+ }
+
+ scope.updateView = function () {
+ if (!scope.disabled) {
+ ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id : null);
+ }
+ }
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ entityService.getRelatedEntity(ngModelCtrl.$viewValue).then(
+ function success(entity) {
+ scope.entity = entity;
+ },
+ function fail() {
+ scope.entity = null;
+ }
+ );
+ } else {
+ scope.entity = null;
+ }
+ }
+
+ scope.$watch('entity', function () {
+ scope.updateView();
+ });
+
+ scope.$watch('disabled', function () {
+ scope.updateView();
+ });
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ link: linker,
+ scope: {
+ rootEntityId: '=',
+ entityType: '=',
+ entitySubtypes: '=',
+ excludeEntityId: '=?',
+ theForm: '=?',
+ tbRequired: '=?',
+ disabled:'=ngDisabled',
+ placeholderText: '@',
+ notFoundText: '@',
+ requiredText: '@'
+ }
+ };
+}
diff --git a/ui/src/app/components/related-entity-autocomplete.scss b/ui/src/app/components/related-entity-autocomplete.scss
new file mode 100644
index 0000000..32df94f
--- /dev/null
+++ b/ui/src/app/components/related-entity-autocomplete.scss
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-related-entity-autocomplete {
+ .tb-not-found {
+ display: block;
+ line-height: 1.5;
+ height: 48px;
+ }
+ .tb-entity-item {
+ display: block;
+ height: 48px;
+ }
+ li {
+ height: auto !important;
+ white-space: normal !important;
+ }
+}
diff --git a/ui/src/app/components/related-entity-autocomplete.tpl.html b/ui/src/app/components/related-entity-autocomplete.tpl.html
new file mode 100644
index 0000000..a5b50e9
--- /dev/null
+++ b/ui/src/app/components/related-entity-autocomplete.tpl.html
@@ -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.
+
+-->
+<md-autocomplete ng-required="tbRequired"
+ ng-disabled="disabled"
+ md-input-name="entity"
+ ng-model="entity"
+ md-selected-item="entity"
+ md-search-text="entitySearchText"
+ md-search-text-change="entitySearchTextChanged()"
+ md-items="item in fetchEntities(entitySearchText)"
+ md-item-text="item.name"
+ md-min-length="0"
+ md-floating-label="{{ placeholderText | translate }}"
+ md-menu-class="tb-related-entity-autocomplete">
+ <md-item-template>
+ <div class="tb-entity-item">
+ <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
+ </div>
+ </md-item-template>
+ <md-not-found>
+ <div class="tb-not-found">
+ <span>{{ notFoundText | translate:{entity: entitySearchText} }}</span>
+ </div>
+ </md-not-found>
+ <div ng-messages="theForm.entity.$error">
+ <div ng-message="required">{{ requiredText | translate }}</div>
+ </div>
+</md-autocomplete>
ui/src/app/components/widget.controller.js 26(+19 -7)
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index 7c7caa9..6fc4bba 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -21,8 +21,8 @@ import Subscription from '../api/subscription';
/*@ngInject*/
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
- datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
- dashboardTimewindowApi, widget, aliasesInfo, widgetType) {
+ datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
+ dashboardTimewindowApi, widget, aliasesInfo, stateController, widgetType) {
var vm = this;
@@ -83,10 +83,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
// type: "timeseries" or "latest" or "rpc"
- /* devicesSubscriptionInfo = [
+ /* subscriptionInfo = [
{
- deviceId: ""
- deviceName: ""
+ entityType: ""
+ entityId: ""
+ entityName: ""
timeseries: [{ name: "", label: "" }, ..]
attributes: [{ name: "", label: "" }, ..]
}
@@ -130,7 +131,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
},
utils: {
formatValue: formatValue
- }
+ },
+ stateController: stateController
};
var subscriptionContext = {
@@ -231,7 +233,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
}
}
- utils.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then(
+ entityService.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then(
function (datasources) {
options.datasources = datasources;
var subscription = createSubscription(options, subscribe);
@@ -341,6 +343,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
},
onRpcSuccess: function(subscription) {
$scope.executingRpcRequest = subscription.executingRpcRequest;
+ $scope.rpcErrorText = subscription.rpcErrorText;
+ $scope.rpcRejection = subscription.rpcRejection;
},
onRpcFailed: function(subscription) {
$scope.executingRpcRequest = subscription.executingRpcRequest;
@@ -394,6 +398,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
onMobileModeChanged(newIsMobile);
});
+ $scope.$on('entityAliasListChanged', function (event, aliasesInfo) {
+ subscriptionContext.aliasesInfo = aliasesInfo;
+ for (var id in widgetContext.subscriptions) {
+ var subscription = widgetContext.subscriptions[id];
+ subscription.onAliasesChanged();
+ }
+ });
+
$scope.$on("$destroy", function () {
removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
onDestroy();
ui/src/app/components/widget-config.directive.js 217(+122 -95)
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
index bcffa72..122889d 100644
--- a/ui/src/app/components/widget-config.directive.js
+++ b/ui/src/app/components/widget-config.directive.js
@@ -16,7 +16,7 @@
import jsonSchemaDefaults from 'json-schema-defaults';
import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service';
-import thingsboardDeviceAliasSelect from './device-alias-select.directive';
+import thingsboardEntityAliasSelect from './entity-alias-select.directive';
import thingsboardDatasource from './datasource.directive';
import thingsboardTimewindow from './timewindow.directive';
import thingsboardLegendConfig from './legend-config.directive';
@@ -34,7 +34,7 @@ import widgetConfigTemplate from './widget-config.tpl.html';
export default angular.module('thingsboard.directives.widgetConfig', [thingsboardTypes,
thingsboardUtils,
thingsboardJsonForm,
- thingsboardDeviceAliasSelect,
+ thingsboardEntityAliasSelect,
thingsboardDatasource,
thingsboardTimewindow,
thingsboardLegendConfig,
@@ -89,58 +89,68 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
- scope.selectedTab = 0;
- scope.title = ngModelCtrl.$viewValue.title;
- scope.showTitle = ngModelCtrl.$viewValue.showTitle;
- scope.dropShadow = angular.isDefined(ngModelCtrl.$viewValue.dropShadow) ? ngModelCtrl.$viewValue.dropShadow : true;
- scope.enableFullscreen = angular.isDefined(ngModelCtrl.$viewValue.enableFullscreen) ? ngModelCtrl.$viewValue.enableFullscreen : true;
- scope.backgroundColor = ngModelCtrl.$viewValue.backgroundColor;
- scope.color = ngModelCtrl.$viewValue.color;
- scope.padding = ngModelCtrl.$viewValue.padding;
- scope.titleStyle =
- angular.toJson(angular.isDefined(ngModelCtrl.$viewValue.titleStyle) ? ngModelCtrl.$viewValue.titleStyle : {
- fontSize: '16px',
- fontWeight: 400
- }, true);
- scope.mobileOrder = ngModelCtrl.$viewValue.mobileOrder;
- scope.mobileHeight = ngModelCtrl.$viewValue.mobileHeight;
- scope.units = ngModelCtrl.$viewValue.units;
- scope.decimals = ngModelCtrl.$viewValue.decimals;
- scope.useDashboardTimewindow = angular.isDefined(ngModelCtrl.$viewValue.useDashboardTimewindow) ?
- ngModelCtrl.$viewValue.useDashboardTimewindow : true;
- scope.timewindow = ngModelCtrl.$viewValue.timewindow;
- scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ?
- ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value;
- scope.legendConfig = ngModelCtrl.$viewValue.legendConfig;
- if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
- && scope.isDataEnabled) {
- if (scope.datasources) {
- scope.datasources.splice(0, scope.datasources.length);
- } else {
- scope.datasources = [];
- }
- if (ngModelCtrl.$viewValue.datasources) {
- for (var i in ngModelCtrl.$viewValue.datasources) {
- scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]});
+ var config = ngModelCtrl.$viewValue.config;
+ var layout = ngModelCtrl.$viewValue.layout;
+ if (config) {
+ scope.selectedTab = 0;
+ scope.title = config.title;
+ scope.showTitle = config.showTitle;
+ scope.dropShadow = angular.isDefined(config.dropShadow) ? config.dropShadow : true;
+ scope.enableFullscreen = angular.isDefined(config.enableFullscreen) ? config.enableFullscreen : true;
+ scope.backgroundColor = config.backgroundColor;
+ scope.color = config.color;
+ scope.padding = config.padding;
+ scope.titleStyle =
+ angular.toJson(angular.isDefined(config.titleStyle) ? config.titleStyle : {
+ fontSize: '16px',
+ fontWeight: 400
+ }, true);
+ scope.units = config.units;
+ scope.decimals = config.decimals;
+ scope.useDashboardTimewindow = angular.isDefined(config.useDashboardTimewindow) ?
+ config.useDashboardTimewindow : true;
+ scope.timewindow = config.timewindow;
+ scope.showLegend = angular.isDefined(config.showLegend) ?
+ config.showLegend : scope.widgetType === types.widgetType.timeseries.value;
+ scope.legendConfig = config.legendConfig;
+ if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
+ && scope.isDataEnabled) {
+ if (scope.datasources) {
+ scope.datasources.splice(0, scope.datasources.length);
+ } else {
+ scope.datasources = [];
}
- }
- } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
- if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
- var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
- if (scope.deviceAliases[aliasId]) {
- scope.targetDeviceAlias.value = {id: aliasId, alias: scope.deviceAliases[aliasId].alias,
- deviceId: scope.deviceAliases[aliasId].deviceId};
+ if (config.datasources) {
+ for (var i in config.datasources) {
+ scope.datasources.push({value: config.datasources[i]});
+ }
+ }
+ } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
+ if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
+ var aliasId = config.targetDeviceAliasIds[0];
+ if (scope.entityAliases[aliasId]) {
+ scope.targetDeviceAlias.value = {
+ id: aliasId,
+ alias: scope.entityAliases[aliasId].alias,
+ entityType: scope.entityAliases[aliasId].entityType,
+ entityId: scope.entityAliases[aliasId].entityId
+ };
+ } else {
+ scope.targetDeviceAlias.value = null;
+ }
} else {
scope.targetDeviceAlias.value = null;
}
- } else {
- scope.targetDeviceAlias.value = null;
}
- }
- scope.settings = ngModelCtrl.$viewValue.settings;
+ scope.settings = config.settings;
- scope.updateSchemaForm();
+ scope.updateSchemaForm();
+ }
+ if (layout) {
+ scope.mobileOrder = layout.mobileOrder;
+ scope.mobileHeight = layout.mobileHeight;
+ }
}
};
@@ -163,19 +173,22 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.updateValidity = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
- var valid;
- if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
- valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
- ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
- } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
- valid = value && value.datasources && value.datasources.length > 0;
- ngModelCtrl.$setValidity('datasources', valid);
- }
- try {
- angular.fromJson(scope.titleStyle);
- ngModelCtrl.$setValidity('titleStyle', true);
- } catch (e) {
- ngModelCtrl.$setValidity('titleStyle', false);
+ var config = value.config;
+ if (config) {
+ var valid;
+ if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
+ valid = config && config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0;
+ ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
+ } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
+ valid = config && config.datasources && config.datasources.length > 0;
+ ngModelCtrl.$setValidity('datasources', valid);
+ }
+ try {
+ angular.fromJson(scope.titleStyle);
+ ngModelCtrl.$setValidity('titleStyle', true);
+ } catch (e) {
+ ngModelCtrl.$setValidity('titleStyle', false);
+ }
}
}
};
@@ -184,24 +197,30 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
'padding + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + showLegend', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
- value.title = scope.title;
- value.showTitle = scope.showTitle;
- value.dropShadow = scope.dropShadow;
- value.enableFullscreen = scope.enableFullscreen;
- value.backgroundColor = scope.backgroundColor;
- value.color = scope.color;
- value.padding = scope.padding;
- try {
- value.titleStyle = angular.fromJson(scope.titleStyle);
- } catch (e) {
- value.titleStyle = {};
+ if (value.config) {
+ var config = value.config;
+ config.title = scope.title;
+ config.showTitle = scope.showTitle;
+ config.dropShadow = scope.dropShadow;
+ config.enableFullscreen = scope.enableFullscreen;
+ config.backgroundColor = scope.backgroundColor;
+ config.color = scope.color;
+ config.padding = scope.padding;
+ try {
+ config.titleStyle = angular.fromJson(scope.titleStyle);
+ } catch (e) {
+ config.titleStyle = {};
+ }
+ config.units = scope.units;
+ config.decimals = scope.decimals;
+ config.useDashboardTimewindow = scope.useDashboardTimewindow;
+ config.showLegend = scope.showLegend;
+ }
+ if (value.layout) {
+ var layout = value.layout;
+ layout.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined;
+ layout.mobileHeight = scope.mobileHeight;
}
- value.mobileOrder = angular.isNumber(scope.mobileOrder) ? scope.mobileOrder : undefined;
- value.mobileHeight = scope.mobileHeight;
- value.units = scope.units;
- value.decimals = scope.decimals;
- value.useDashboardTimewindow = scope.useDashboardTimewindow;
- value.showLegend = scope.showLegend;
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
@@ -210,39 +229,46 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.$watch('currentSettings', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
- value.settings = scope.currentSettings;
- ngModelCtrl.$setViewValue(value);
+ if (value.config) {
+ value.config.settings = scope.currentSettings;
+ ngModelCtrl.$setViewValue(value);
+ }
}
}, true);
scope.$watch('timewindow', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
- value.timewindow = scope.timewindow;
- ngModelCtrl.$setViewValue(value);
+ if (value.config) {
+ value.config.timewindow = scope.timewindow;
+ ngModelCtrl.$setViewValue(value);
+ }
}
}, true);
scope.$watch('legendConfig', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
- value.legendConfig = scope.legendConfig;
- ngModelCtrl.$setViewValue(value);
+ if (value.config) {
+ value.config.legendConfig = scope.legendConfig;
+ ngModelCtrl.$setViewValue(value);
+ }
}
}, true);
scope.$watch('datasources', function () {
- if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value
+ if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType !== types.widgetType.rpc.value
&& scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue;
- if (value.datasources) {
- value.datasources.splice(0, value.datasources.length);
+ var config = value.config;
+ if (config.datasources) {
+ config.datasources.splice(0, config.datasources.length);
} else {
- value.datasources = [];
+ config.datasources = [];
}
if (scope.datasources) {
for (var i in scope.datasources) {
- value.datasources.push(scope.datasources[i].value);
+ config.datasources.push(scope.datasources[i].value);
}
}
ngModelCtrl.$setViewValue(value);
@@ -251,12 +277,13 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}, true);
scope.$watch('targetDeviceAlias.value', function () {
- if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
+ if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue;
+ var config = value.config;
if (scope.targetDeviceAlias.value) {
- value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
+ config.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
} else {
- value.targetDeviceAliasIds = [];
+ config.targetDeviceAliasIds = [];
}
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
@@ -269,7 +296,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
newDatasource = angular.copy(utils.getDefaultDatasource(scope.datakeySettingsSchema.schema));
newDatasource.dataKeys = [scope.generateDataKey('Sin', types.dataKeyType.function)];
} else {
- newDatasource = { type: types.datasourceType.device,
+ newDatasource = { type: types.datasourceType.entity,
dataKeys: []
};
}
@@ -368,10 +395,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
widgetType: '=',
widgetSettingsSchema: '=',
datakeySettingsSchema: '=',
- deviceAliases: '=',
+ entityAliases: '=',
functionsOnly: '=',
- fetchDeviceKeys: '&',
- onCreateDeviceAlias: '&',
+ fetchEntityKeys: '&',
+ onCreateEntityAlias: '&',
theForm: '='
},
link: linker
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
index e7c03e3..808e07b 100644
--- a/ui/src/app/components/widget-config.tpl.html
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -60,12 +60,12 @@
style="padding: 0 0 0 10px; margin: 5px;">
<tb-datasource flex ng-model="datasource.value"
widget-type="widgetType"
- device-aliases="deviceAliases"
+ entity-aliases="entityAliases"
functions-only="functionsOnly"
datakey-settings-schema="datakeySettingsSchema"
generate-data-key="generateDataKey(chip,type)"
- fetch-device-keys="fetchDeviceKeys({deviceAliasId: deviceAliasId, query: query, type: type})"
- on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})"></tb-datasource>
+ fetch-entity-keys="fetchEntityKeys({entityAliasId: entityAliasId, query: query, type: type})"
+ on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})"></tb-datasource>
<md-button ng-disabled="loading" class="md-icon-button md-primary"
style="min-width: 40px;"
ng-click="removeDatasource($event, datasource)"
@@ -102,12 +102,13 @@
{{ 'widget-config.target-device' | translate }}
</v-pane-header>
<v-pane-content style="padding: 0 5px;">
- <tb-device-alias-select flex
+ <tb-entity-alias-select flex
tb-required="widgetType === types.widgetType.rpc.value && !widgetEditMode"
- device-aliases="deviceAliases"
+ entity-aliases="entityAliases"
+ allowed-entity-types="[types.entityType.device]"
ng-model="targetDeviceAlias.value"
- on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
- </tb-device-alias-select>
+ on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias, allowedEntityTypes: allowedEntityTypes})">
+ </tb-entity-alias-select>
</v-pane-content>
</v-pane>
</v-accordion>
ui/src/app/customer/customer.controller.js 26(+25 -1)
diff --git a/ui/src/app/customer/customer.controller.js b/ui/src/app/customer/customer.controller.js
index 4182588..5175052 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 = [
{
@@ -37,6 +37,20 @@ export default function CustomerController(customerService, $state, $stateParams
},
{
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') },
@@ -78,6 +92,8 @@ export default function CustomerController(customerService, $state, $stateParams
var vm = this;
+ vm.types = types;
+
vm.customerGridConfig = {
refreshParamsFunc: null,
@@ -128,6 +144,7 @@ export default function CustomerController(customerService, $state, $stateParams
}
vm.openCustomerUsers = openCustomerUsers;
+ vm.openCustomerAssets = openCustomerAssets;
vm.openCustomerDevices = openCustomerDevices;
vm.openCustomerDashboards = openCustomerDashboards;
@@ -178,6 +195,13 @@ export default function CustomerController(customerService, $state, $stateParams
$state.go('home.customers.users', {customerId: customer.id.id});
}
+ function openCustomerAssets($event, customer) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.customers.assets', {customerId: customer.id.id});
+ }
+
function openCustomerDevices($event, customer) {
if ($event) {
$event.stopPropagation();
diff --git a/ui/src/app/customer/customer.directive.js b/ui/src/app/customer/customer.directive.js
index 4a4e09c..e48934b 100644
--- a/ui/src/app/customer/customer.directive.js
+++ b/ui/src/app/customer/customer.directive.js
@@ -20,13 +20,17 @@ import customerFieldsetTemplate from './customer-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function CustomerDirective($compile, $templateCache) {
+export default function CustomerDirective($compile, $templateCache, $translate, toast) {
var linker = function (scope, element) {
var template = $templateCache.get(customerFieldsetTemplate);
element.html(template);
scope.isPublic = false;
+ scope.onCustomerIdCopied = function() {
+ toast.showSuccess($translate.instant('customer.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+ };
+
scope.$watch('customer', function(newVal) {
if (newVal) {
if (scope.customer.additionalInfo) {
@@ -48,6 +52,7 @@ export default function CustomerDirective($compile, $templateCache) {
isEdit: '=',
theForm: '=',
onManageUsers: '&',
+ onManageAssets: '&',
onManageDevices: '&',
onManageDashboards: '&',
onDeleteCustomer: '&'
diff --git a/ui/src/app/customer/customer-fieldset.tpl.html b/ui/src/app/customer/customer-fieldset.tpl.html
index a216571..3facd0f 100644
--- a/ui/src/app/customer/customer-fieldset.tpl.html
+++ b/ui/src/app/customer/customer-fieldset.tpl.html
@@ -16,10 +16,21 @@
-->
<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button>
+<md-button ng-click="onManageAssets({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.manage-assets' | translate }}</md-button>
<md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button>
<md-button ng-click="onManageDashboards({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-dashboards' | translate }}</md-button>
<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button>
+<div layout="row">
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onCustomerIdCopied(e)"
+ data-clipboard-text="{{customer.id.id}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>customer.copyId</span>
+ </md-button>
+</div>
+
<md-content class="md-padding" layout="column">
<fieldset ng-show="!isPublic" ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
ui/src/app/customer/customers.tpl.html 44(+37 -7)
diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html
index 7d706be..9221984 100644
--- a/ui/src/app/customer/customers.tpl.html
+++ b/ui/src/app/customer/customers.tpl.html
@@ -19,11 +19,41 @@
<details-buttons tb-help="'customers'" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
- <tb-customer customer="vm.grid.operatingItem()"
- is-edit="vm.grid.detailsConfig.isDetailsEditMode"
- the-form="vm.grid.detailsForm"
- on-manage-users="vm.openCustomerUsers(event, vm.grid.detailsConfig.currentItem)"
- on-manage-devices="vm.openCustomerDevices(event, vm.grid.detailsConfig.currentItem)"
- on-manage-dashboards="vm.openCustomerDashboards(event, vm.grid.detailsConfig.currentItem)"
- on-delete-customer="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-customer>
+ <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'customer.details' | translate }}">
+ <tb-customer customer="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ the-form="vm.grid.detailsForm"
+ on-manage-users="vm.openCustomerUsers(event, vm.grid.detailsConfig.currentItem)"
+ on-manage-assets="vm.openCustomerAssets(event, vm.grid.detailsConfig.currentItem)"
+ on-manage-devices="vm.openCustomerDevices(event, vm.grid.detailsConfig.currentItem)"
+ on-manage-dashboards="vm.openCustomerDashboards(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-customer="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-customer>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.customer}}"
+ entity-name="vm.grid.operatingItem().title"
+ default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.customer}}"
+ entity-name="vm.grid.operatingItem().title"
+ default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+ disable-attribute-scope-selection="true">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'customer.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.customer"
+ entity-id="vm.grid.operatingItem().id.id"
+ tenant-id="vm.grid.operatingItem().tenantId.id"
+ default-event-type="{{vm.types.eventType.alarm.value}}">
+ </tb-event-table>
+ </md-tab>
+ </md-tabs>
</tb-grid>
ui/src/app/dashboard/add-widget.controller.js 53(+31 -22)
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index e9ceaad..2f23cf9 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -15,12 +15,12 @@
*/
/* eslint-disable import/no-unresolved, import/default */
-import deviceAliasesTemplate from './device-aliases.tpl.html';
+import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) {
+export default function AddWidgetController($scope, widgetService, entityService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) {
var vm = this;
@@ -34,10 +34,16 @@ export default function AddWidgetController($scope, widgetService, deviceService
vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
vm.add = add;
vm.cancel = cancel;
- vm.fetchDeviceKeys = fetchDeviceKeys;
- vm.createDeviceAlias = createDeviceAlias;
+ vm.fetchEntityKeys = fetchEntityKeys;
+ vm.createEntityAlias = createEntityAlias;
- vm.widgetConfig = vm.widget.config;
+ vm.widgetConfig = {
+ config: vm.widget.config,
+ layout: {}
+ };
+
+ vm.widgetConfig.layout.mobileOrder = vm.widget.config.mobileOrder;
+ vm.widgetConfig.layout.mobileHeight = vm.widget.config.mobileHeight;
var settingsSchema = vm.widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
var dataKeySettingsSchema = vm.widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
@@ -85,50 +91,53 @@ export default function AddWidgetController($scope, widgetService, deviceService
function add () {
if ($scope.theForm.$valid) {
$scope.theForm.$setPristine();
- vm.widget.config = vm.widgetConfig;
+ vm.widget.config = vm.widgetConfig.config;
+ vm.widget.config.mobileOrder = vm.widgetConfig.layout.mobileOrder;
+ vm.widget.config.mobileHeight = vm.widgetConfig.layout.mobileHeight;
$mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo});
}
}
- function fetchDeviceKeys (deviceAliasId, query, type) {
- var deviceAlias = vm.aliasesInfo.deviceAliases[deviceAliasId];
- if (deviceAlias && deviceAlias.deviceId) {
- return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
+ function fetchEntityKeys (entityAliasId, query, type) {
+ var entityAlias = vm.aliasesInfo.entityAliases[entityAliasId];
+ if (entityAlias && entityAlias.entityId) {
+ return entityService.getEntityKeys(entityAlias.entityType, entityAlias.entityId, query, type);
} else {
return $q.when([]);
}
}
- function createDeviceAlias (event, alias) {
+ function createEntityAlias (event, alias, allowedEntityTypes) {
var deferred = $q.defer();
- var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
+ var singleEntityAlias = {id: null, alias: alias, entityType: types.entityType.device, entityFilter: null};
$mdDialog.show({
- controller: 'DeviceAliasesController',
+ controller: 'EntityAliasesController',
controllerAs: 'vm',
- templateUrl: deviceAliasesTemplate,
+ templateUrl: entityAliasesTemplate,
locals: {
config: {
- deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+ entityAliases: angular.copy(vm.dashboard.configuration.entityAliases),
widgets: null,
- isSingleDeviceAlias: true,
- singleDeviceAlias: singleDeviceAlias
+ isSingleEntityAlias: true,
+ singleEntityAlias: singleEntityAlias,
+ allowedEntityTypes: allowedEntityTypes
}
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event
- }).then(function (singleDeviceAlias) {
- vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
- { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
- deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases).then(
+ }).then(function (singleEntityAlias) {
+ vm.dashboard.configuration.entityAliases[singleEntityAlias.id] =
+ { alias: singleEntityAlias.alias, entityType: singleEntityAlias.entityType, entityFilter: singleEntityAlias.entityFilter };
+ entityService.processEntityAliases(vm.dashboard.configuration.entityAliases).then(
function(resolution) {
if (!resolution.error) {
vm.aliasesInfo = resolution.aliasesInfo;
}
- deferred.resolve(singleDeviceAlias);
+ deferred.resolve(singleEntityAlias);
}
);
}, function () {
diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html
index 4e6db50..870e203 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"></tb-widget-config>
</fieldset>
</div>
ui/src/app/dashboard/dashboard.controller.js 743(+502 -241)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 9121f6a..e18ebb8 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -15,23 +15,28 @@
*/
/* eslint-disable import/no-unresolved, import/default */
-import deviceAliasesTemplate from './device-aliases.tpl.html';
-import dashboardBackgroundTemplate from './dashboard-settings.tpl.html';
+import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
+import dashboardSettingsTemplate from './dashboard-settings.tpl.html';
+import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html';
+import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html';
import addWidgetTemplate from './add-widget.tpl.html';
+import selectTargetLayoutTemplate from './layouts/select-target-layout.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function DashboardController(types, widgetService, userService,
- dashboardService, timeService, deviceService, itembuffer, importExport, hotkeys, $window, $rootScope,
- $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
+export default function DashboardController(types, dashboardUtils, widgetService, userService,
+ dashboardService, timeService, entityService, itembuffer, importExport, hotkeys, $window, $rootScope,
+ $scope, $element, $state, $stateParams, $mdDialog, $mdMedia, $timeout, $document, $q, $translate, $filter) {
var vm = this;
vm.user = userService.getCurrentUser();
vm.dashboard = null;
vm.editingWidget = null;
+ vm.editingWidgetLayout = null;
vm.editingWidgetOriginal = null;
+ vm.editingWidgetLayoutOriginal = null;
vm.editingWidgetSubtitle = null;
vm.forceDashboardMobileMode = false;
vm.isAddingWidget = false;
@@ -43,8 +48,6 @@ export default function DashboardController(types, widgetService, userService,
vm.staticWidgetTypes = [];
vm.widgetEditMode = $state.$current.data.widgetEditMode;
vm.iframeMode = $rootScope.iframeMode;
- vm.widgets = [];
- vm.dashboardInitComplete = false;
vm.isToolbarOpened = false;
@@ -60,10 +63,33 @@ export default function DashboardController(types, widgetService, userService,
}
Object.defineProperty(vm, 'toolbarOpened', {
- get: function() { return vm.isToolbarOpened || vm.isEdit; },
+ get: function() { return !vm.widgetEditMode && ($scope.forceFullscreen || vm.isToolbarOpened || vm.isEdit || vm.showRightLayoutSwitch()); },
set: function() { }
});
+ vm.layouts = {
+ main: {
+ show: false,
+ layoutCtx: {
+ id: 'main',
+ widgets: [],
+ widgetLayouts: {},
+ gridSettings: {},
+ ignoreLoading: false
+ }
+ },
+ right: {
+ show: false,
+ layoutCtx: {
+ id: 'right',
+ widgets: [],
+ widgetLayouts: {},
+ gridSettings: {},
+ ignoreLoading: false
+ }
+ }
+ };
+
vm.openToolbar = function() {
$timeout(function() {
vm.isToolbarOpened = true;
@@ -76,31 +102,78 @@ export default function DashboardController(types, widgetService, userService,
});
}
+ vm.showCloseToolbar = function() {
+ return !$scope.forceFullscreen && !vm.isEdit && !vm.showRightLayoutSwitch();
+ }
+
+ vm.showRightLayoutSwitch = function() {
+ return vm.isMobile && vm.layouts.right.show;
+ }
+
+ vm.toggleLayouts = function() {
+ vm.isRightLayoutOpened = !vm.isRightLayoutOpened;
+ }
+
+ vm.openRightLayout = function() {
+ vm.isRightLayoutOpened = true;
+ }
+
+ vm.isRightLayoutOpened = false;
+ vm.isMobile = !$mdMedia('gt-sm');
+
+ $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) {
+ vm.isMobile = !isGtSm;
+ });
+
+ vm.mainLayoutWidth = function() {
+ if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') {
+ return '100%';
+ } else {
+ return vm.layouts.right.show && !vm.isMobile ? '50%' : '100%';
+ }
+ }
+
+ vm.mainLayoutHeight = function() {
+ if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'main') {
+ return '100%';
+ } else {
+ return 'auto';
+ }
+ }
+
+ vm.rightLayoutWidth = function() {
+ if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') {
+ return '100%';
+ } else {
+ return vm.isMobile ? '100%' : '50%';
+ }
+ }
+
+ vm.rightLayoutHeight = function() {
+ if (vm.isEditingWidget && vm.editingLayoutCtx.id === 'right') {
+ return '100%';
+ } else {
+ return 'auto';
+ }
+ }
+
+ vm.getServerTimeDiff = getServerTimeDiff;
vm.addWidget = addWidget;
vm.addWidgetFromType = addWidgetFromType;
- vm.dashboardInited = dashboardInited;
- vm.dashboardInitFailed = dashboardInitFailed;
- vm.widgetMouseDown = widgetMouseDown;
- vm.widgetClicked = widgetClicked;
- vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
- vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
- vm.editWidget = editWidget;
vm.exportDashboard = exportDashboard;
- vm.exportWidget = exportWidget;
vm.importWidget = importWidget;
vm.isPublicUser = isPublicUser;
vm.isTenantAdmin = isTenantAdmin;
vm.isSystemAdmin = isSystemAdmin;
- vm.loadDashboard = loadDashboard;
- vm.getServerTimeDiff = getServerTimeDiff;
- vm.noData = noData;
vm.dashboardConfigurationError = dashboardConfigurationError;
vm.showDashboardToolbar = showDashboardToolbar;
vm.onAddWidgetClosed = onAddWidgetClosed;
vm.onEditWidgetClosed = onEditWidgetClosed;
- vm.openDeviceAliases = openDeviceAliases;
+ vm.openDashboardState = openDashboardState;
+ vm.openEntityAliases = openEntityAliases;
vm.openDashboardSettings = openDashboardSettings;
- vm.removeWidget = removeWidget;
+ vm.manageDashboardLayouts = manageDashboardLayouts;
+ vm.manageDashboardStates = manageDashboardStates;
vm.saveDashboard = saveDashboard;
vm.saveWidget = saveWidget;
vm.toggleDashboardEditMode = toggleDashboardEditMode;
@@ -109,10 +182,56 @@ export default function DashboardController(types, widgetService, userService,
vm.displayTitle = displayTitle;
vm.displayExport = displayExport;
vm.displayDashboardTimewindow = displayDashboardTimewindow;
- vm.displayDevicesSelect = displayDevicesSelect;
+ vm.displayDashboardsSelect = displayDashboardsSelect;
+ vm.displayEntitiesSelect = displayEntitiesSelect;
vm.widgetsBundle;
+ vm.dashboardCtx = {
+ state: null,
+ stateController: {
+ openRightLayout: function() {
+ vm.openRightLayout();
+ }
+ },
+ onAddWidget: function(event, layoutCtx) {
+ addWidget(event, layoutCtx);
+ },
+ onEditWidget: function(event, layoutCtx, widget) {
+ editWidget(event, layoutCtx, widget);
+ },
+ onExportWidget: function(event, layoutCtx, widget) {
+ exportWidget(event, layoutCtx, widget);
+ },
+ onWidgetMouseDown: function(event, layoutCtx, widget) {
+ widgetMouseDown(event, layoutCtx, widget);
+ },
+ onWidgetClicked: function(event, layoutCtx, widget) {
+ widgetClicked(event, layoutCtx, widget);
+ },
+ prepareDashboardContextMenu: function(layoutCtx) {
+ return prepareDashboardContextMenu(layoutCtx);
+ },
+ prepareWidgetContextMenu: function(layoutCtx, widget) {
+ return prepareWidgetContextMenu(layoutCtx, widget);
+ },
+ onRemoveWidget: function(event, layoutCtx, widget) {
+ removeWidget(event, layoutCtx, widget);
+ },
+ copyWidget: function($event, layoutCtx, widget) {
+ copyWidget($event, layoutCtx, widget);
+ },
+ copyWidgetReference: function($event, layoutCtx, widget) {
+ copyWidgetReference($event, layoutCtx, widget);
+ },
+ pasteWidget: function($event, layoutCtx, pos) {
+ pasteWidget($event, layoutCtx, pos);
+ },
+ pasteWidgetReference: function($event, layoutCtx, pos) {
+ pasteWidgetReference($event, layoutCtx, pos);
+ }
+ };
+
$scope.$watch('vm.widgetsBundle', function (newVal, prevVal) {
if (newVal !== prevVal && !vm.widgetEditMode) {
loadWidgetLibrary();
@@ -132,6 +251,7 @@ export default function DashboardController(types, widgetService, userService,
}
});
+ loadDashboard();
function loadWidgetLibrary() {
vm.latestWidgetTypes = [];
@@ -199,84 +319,100 @@ export default function DashboardController(types, widgetService, userService,
}
function loadDashboard() {
-
- var deferred = $q.defer();
-
if (vm.widgetEditMode) {
- $timeout(function () {
- vm.dashboardConfiguration = {
- timewindow: timeService.defaultTimewindow()
- };
- vm.widgets = [{
- isSystemType: true,
- bundleAlias: 'customWidgetBundle',
- typeAlias: 'customWidget',
- type: $rootScope.editWidgetInfo.type,
- title: 'My widget',
- sizeX: $rootScope.editWidgetInfo.sizeX * 2,
- sizeY: $rootScope.editWidgetInfo.sizeY * 2,
- row: 2,
- col: 4,
- config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig)
- }];
- vm.widgets[0].config.title = vm.widgets[0].config.title || $rootScope.editWidgetInfo.widgetName;
- deferred.resolve();
- var parentScope = $window.parent.angular.element($window.frameElement).scope();
- parentScope.$root.$broadcast('widgetEditModeInited');
- parentScope.$root.$apply();
- });
+ var widget = {
+ isSystemType: true,
+ bundleAlias: 'customWidgetBundle',
+ typeAlias: 'customWidget',
+ type: $rootScope.editWidgetInfo.type,
+ title: 'My widget',
+ sizeX: $rootScope.editWidgetInfo.sizeX * 2,
+ sizeY: $rootScope.editWidgetInfo.sizeY * 2,
+ row: 2,
+ col: 4,
+ config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig)
+ };
+ widget.config.title = widget.config.title || $rootScope.editWidgetInfo.widgetName;
+
+ vm.dashboard = dashboardUtils.createSingleWidgetDashboard(widget);
+ vm.dashboardConfiguration = vm.dashboard.configuration;
+ vm.dashboardCtx.dashboard = vm.dashboard;
+ vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
+ var parentScope = $window.parent.angular.element($window.frameElement).scope();
+ parentScope.$root.$broadcast('widgetEditModeInited');
+ parentScope.$root.$apply();
} else {
-
dashboardService.getDashboard($stateParams.dashboardId)
.then(function success(dashboard) {
- vm.dashboard = dashboard;
- if (vm.dashboard.configuration == null) {
- vm.dashboard.configuration = {widgets: [], deviceAliases: {}};
- }
- if (angular.isUndefined(vm.dashboard.configuration.widgets)) {
- vm.dashboard.configuration.widgets = [];
- }
- if (angular.isUndefined(vm.dashboard.configuration.deviceAliases)) {
- vm.dashboard.configuration.deviceAliases = {};
- }
-
- if (angular.isUndefined(vm.dashboard.configuration.timewindow)) {
- vm.dashboard.configuration.timewindow = timeService.defaultTimewindow();
- }
- deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
+ vm.dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
+ entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
.then(
function(resolution) {
if (resolution.error && !isTenantAdmin()) {
vm.configurationError = true;
showAliasesResolutionError(resolution.error);
- deferred.reject();
} else {
- vm.aliasesInfo = resolution.aliasesInfo;
vm.dashboardConfiguration = vm.dashboard.configuration;
- vm.widgets = vm.dashboard.configuration.widgets;
- deferred.resolve();
+ vm.dashboardCtx.dashboard = vm.dashboard;
+ vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo;
+ vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
}
}
);
- }, function fail(e) {
- deferred.reject(e);
+ }, function fail() {
+ vm.configurationError = true;
});
-
}
- return deferred.promise;
}
- function dashboardInitFailed() {
- var parentScope = $window.parent.angular.element($window.frameElement).scope();
- parentScope.$emit('widgetEditModeInited');
- parentScope.$apply();
- vm.dashboardInitComplete = true;
+ function openDashboardState(state) {
+ var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, state);
+ if (layoutsData) {
+ vm.dashboardCtx.state = state;
+ var layoutVisibilityChanged = false;
+ for (var l in vm.layouts) {
+ var layout = vm.layouts[l];
+ var showLayout;
+ if (layoutsData[l]) {
+ showLayout = true;
+ } else {
+ showLayout = false;
+ }
+ if (layout.show != showLayout) {
+ layout.show = showLayout;
+ layoutVisibilityChanged = !vm.isMobile;
+ }
+ }
+ vm.isRightLayoutOpened = false;
+ updateLayouts(layoutVisibilityChanged);
+ }
+
+ function updateLayouts(layoutVisibilityChanged) {
+ for (l in vm.layouts) {
+ layout = vm.layouts[l];
+ if (layoutsData[l]) {
+ var layoutInfo = layoutsData[l];
+ if (layout.layoutCtx.id === 'main') {
+ layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged);
+ }
+ updateLayout(layout, layoutInfo.widgets, layoutInfo.widgetLayouts, layoutInfo.gridSettings);
+ } else {
+ updateLayout(layout, [], {}, null);
+ }
+ }
+ }
}
- function dashboardInited(dashboard) {
- vm.dashboardContainer = dashboard;
- initHotKeys();
- vm.dashboardInitComplete = true;
+ function updateLayout(layout, widgets, widgetLayouts, gridSettings) {
+ if (gridSettings) {
+ layout.layoutCtx.gridSettings = gridSettings;
+ }
+ layout.layoutCtx.widgets = widgets;
+ layout.layoutCtx.widgetLayouts = widgetLayouts;
+ if (layout.show && layout.layoutCtx.ctrl) {
+ layout.layoutCtx.ctrl.reload();
+ }
+ layout.layoutCtx.ignoreLoading = true;
}
function isPublicUser() {
@@ -291,91 +427,148 @@ export default function DashboardController(types, widgetService, userService,
return vm.user.authority === 'SYS_ADMIN';
}
- function noData() {
- return vm.dashboardInitComplete && !vm.configurationError && vm.widgets.length == 0;
- }
-
function dashboardConfigurationError() {
- return vm.dashboardInitComplete && vm.configurationError;
+ return vm.configurationError;
}
function showDashboardToolbar() {
- return vm.dashboardInitComplete;
+ return true;
}
- function openDeviceAliases($event) {
+ function openEntityAliases($event) {
$mdDialog.show({
- controller: 'DeviceAliasesController',
+ controller: 'EntityAliasesController',
controllerAs: 'vm',
- templateUrl: deviceAliasesTemplate,
+ templateUrl: entityAliasesTemplate,
locals: {
config: {
- deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
- widgets: vm.widgets,
- isSingleDeviceAlias: false,
- singleDeviceAlias: null
+ entityAliases: angular.copy(vm.dashboard.configuration.entityAliases),
+ widgets: dashboardUtils.getWidgetsArray(vm.dashboard),
+ isSingleEntityAlias: false,
+ singleEntityAlias: null
}
},
parent: angular.element($document[0].body),
skipHide: true,
fullscreen: true,
targetEvent: $event
- }).then(function (deviceAliases) {
- vm.dashboard.configuration.deviceAliases = deviceAliases;
- deviceAliasesUpdated();
+ }).then(function (entityAliases) {
+ vm.dashboard.configuration.entityAliases = entityAliases;
+ entityAliasesUpdated();
}, function () {
});
}
function openDashboardSettings($event) {
+ var gridSettings = null;
+ var layoutKeys = dashboardUtils.isSingleLayoutDashboard(vm.dashboard);
+ if (layoutKeys) {
+ gridSettings = angular.copy(vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings)
+ }
$mdDialog.show({
controller: 'DashboardSettingsController',
controllerAs: 'vm',
- templateUrl: dashboardBackgroundTemplate,
+ templateUrl: dashboardSettingsTemplate,
locals: {
- gridSettings: angular.copy(vm.dashboard.configuration.gridSettings)
+ settings: angular.copy(vm.dashboard.configuration.settings),
+ gridSettings: gridSettings
},
parent: angular.element($document[0].body),
skipHide: true,
fullscreen: true,
targetEvent: $event
- }).then(function (gridSettings) {
- var prevGridSettings = vm.dashboard.configuration.gridSettings;
- var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
- var ratio = gridSettings.columns / prevColumns;
- var currentWidgets = angular.copy(vm.widgets);
- vm.widgets = [];
- vm.dashboard.configuration.gridSettings = gridSettings;
- for (var w in currentWidgets) {
- var widget = currentWidgets[w];
- widget.sizeX = Math.round(widget.sizeX * ratio);
- widget.sizeY = Math.round(widget.sizeY * ratio);
- widget.col = Math.round(widget.col * ratio);
- widget.row = Math.round(widget.row * ratio);
+ }).then(function (data) {
+ vm.dashboard.configuration.settings = data.settings;
+ var gridSettings = data.gridSettings;
+ if (gridSettings) {
+ updateLayoutGrid(layoutKeys, gridSettings);
}
- vm.dashboard.configuration.widgets = currentWidgets;
- vm.widgets = vm.dashboard.configuration.widgets;
}, function () {
});
}
- function editWidget($event, widget) {
+ function manageDashboardLayouts($event) {
+ $mdDialog.show({
+ controller: 'ManageDashboardLayoutsController',
+ controllerAs: 'vm',
+ templateUrl: manageDashboardLayoutsTemplate,
+ locals: {
+ layouts: angular.copy(vm.dashboard.configuration.states[vm.dashboardCtx.state].layouts)
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (layouts) {
+ updateLayouts(layouts);
+ }, function () {
+ });
+ }
+
+ function manageDashboardStates($event) {
+ var dashboardConfiguration = vm.dashboard.configuration;
+ var states = angular.copy(dashboardConfiguration.states);
+
+ $mdDialog.show({
+ controller: 'ManageDashboardStatesController',
+ controllerAs: 'vm',
+ templateUrl: manageDashboardStatesTemplate,
+ locals: {
+ states: states
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (states) {
+ updateStates(states);
+ }, function () {
+ });
+ }
+
+ function updateLayoutGrid(layoutKeys, gridSettings) {
+ var layout = vm.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
+ var layoutCtx = vm.layouts[layoutKeys.layout];
+ layoutCtx.widgets = [];
+ dashboardUtils.updateLayoutSettings(layout, gridSettings);
+ var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, layoutKeys.state);
+ layoutCtx.widgets = layoutsData[layoutKeys.layout].widgets;
+ }
+
+ function updateLayouts(layouts) {
+ dashboardUtils.setLayouts(vm.dashboard, vm.dashboardCtx.state, layouts);
+ openDashboardState(vm.dashboardCtx.state);
+ }
+
+ function updateStates(states) {
+ vm.dashboard.configuration.states = states;
+ dashboardUtils.removeUnusedWidgets(vm.dashboard);
+ var targetState = vm.dashboardCtx.state;
+ if (!vm.dashboard.configuration.states[targetState]) {
+ targetState = dashboardUtils.getRootStateId(vm.dashboardConfiguration.states);
+ }
+ openDashboardState(targetState);
+ }
+
+ function editWidget($event, layoutCtx, widget) {
$event.stopPropagation();
if (vm.editingWidgetOriginal === widget) {
$timeout(onEditWidgetClosed());
} else {
var transition = !vm.forceDashboardMobileMode;
vm.editingWidgetOriginal = widget;
+ vm.editingWidgetLayoutOriginal = layoutCtx.widgetLayouts[widget.id];
vm.editingWidget = angular.copy(vm.editingWidgetOriginal);
+ vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal);
+ vm.editingLayoutCtx = layoutCtx;
vm.editingWidgetSubtitle = widgetService.getInstantWidgetInfo(vm.editingWidget).widgetName;
vm.forceDashboardMobileMode = true;
vm.isEditingWidget = true;
-
- if (vm.dashboardContainer) {
+ if (layoutCtx) {
var delayOffset = transition ? 350 : 0;
var delay = transition ? 400 : 300;
$timeout(function () {
- vm.dashboardContainer.highlightWidget(vm.editingWidgetOriginal, delay);
+ layoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, delay);
}, delayOffset, false);
}
}
@@ -385,82 +578,36 @@ export default function DashboardController(types, widgetService, userService,
importExport.exportDashboard(vm.currentDashboardId);
}
- function exportWidget($event, widget) {
+ function exportWidget($event, layoutCtx, widget) {
$event.stopPropagation();
- importExport.exportWidget(vm.dashboard, widget);
+ importExport.exportWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget);
}
function importWidget($event) {
$event.stopPropagation();
- importExport.importWidget($event, vm.dashboard, deviceAliasesUpdated);
+ importExport.importWidget($event, vm.dashboard, vm.dashboardCtx.state,
+ selectTargetLayout, entityAliasesUpdated).then(
+ function success(importData) {
+ var widget = importData.widget;
+ var layoutId = importData.layoutId;
+ vm.layouts[layoutId].layoutCtx.widgets.push(widget);
+ }
+ );
}
- function widgetMouseDown($event, widget) {
+ function widgetMouseDown($event, layoutCtx, widget) {
if (vm.isEdit && !vm.isEditingWidget) {
- vm.dashboardContainer.selectWidget(widget, 0);
+ layoutCtx.ctrl.selectWidget(widget, 0);
}
}
- function widgetClicked($event, widget) {
+ function widgetClicked($event, layoutCtx, widget) {
if (vm.isEditingWidget) {
- editWidget($event, widget);
+ editWidget($event, layoutCtx, widget);
}
}
- function isHotKeyAllowed(event) {
- var target = event.target || event.srcElement;
- var scope = angular.element(target).scope();
- return scope && scope.$parent !== $rootScope;
- }
-
- function initHotKeys() {
- $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) {
- hotkeys.bindTo($scope)
- .add({
- combo: 'ctrl+c',
- description: translations['action.copy'],
- callback: function (event) {
- if (isHotKeyAllowed(event) &&
- vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
- var widget = vm.dashboardContainer.getSelectedWidget();
- if (widget) {
- event.preventDefault();
- copyWidget(event, widget);
- }
- }
- }
- })
- .add({
- combo: 'ctrl+v',
- description: translations['action.paste'],
- callback: function (event) {
- if (isHotKeyAllowed(event) &&
- vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
- if (itembuffer.hasWidget()) {
- event.preventDefault();
- pasteWidget(event);
- }
- }
- }
- })
- .add({
- combo: 'ctrl+x',
- description: translations['action.delete'],
- callback: function (event) {
- if (isHotKeyAllowed(event) &&
- vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
- var widget = vm.dashboardContainer.getSelectedWidget();
- if (widget) {
- event.preventDefault();
- removeWidget(event, widget);
- }
- }
- }
- });
- });
- }
-
- function prepareDashboardContextMenu() {
+ function prepareDashboardContextMenu(layoutCtx) {
var dashboardContextActions = [];
if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
dashboardContextActions.push(
@@ -473,36 +620,47 @@ export default function DashboardController(types, widgetService, userService,
);
dashboardContextActions.push(
{
- action: openDeviceAliases,
+ action: openEntityAliases,
enabled: true,
- value: "device.aliases",
+ value: "entity.aliases",
icon: "devices_other"
}
);
dashboardContextActions.push(
{
- action: pasteWidget,
+ action: function ($event) {
+ layoutCtx.ctrl.pasteWidget($event);
+ },
enabled: itembuffer.hasWidget(),
value: "action.paste",
icon: "content_paste",
shortcut: "M-V"
}
);
+ dashboardContextActions.push(
+ {
+ action: function ($event) {
+ layoutCtx.ctrl.pasteWidgetReference($event);
+ },
+ enabled: itembuffer.canPasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id),
+ value: "action.paste-reference",
+ icon: "content_paste",
+ shortcut: "M-I"
+ }
+ );
+
}
return dashboardContextActions;
}
- function pasteWidget($event) {
- var pos = vm.dashboardContainer.getEventGridPosition($event);
- itembuffer.pasteWidget(vm.dashboard, pos, deviceAliasesUpdated);
- }
-
- function prepareWidgetContextMenu() {
+ function prepareWidgetContextMenu(layoutCtx) {
var widgetContextActions = [];
if (vm.isEdit && !vm.isEditingWidget) {
widgetContextActions.push(
{
- action: editWidget,
+ action: function (event, widget) {
+ editWidget(event, layoutCtx, widget);
+ },
enabled: true,
value: "action.edit",
icon: "edit"
@@ -511,7 +669,9 @@ export default function DashboardController(types, widgetService, userService,
if (!vm.widgetEditMode) {
widgetContextActions.push(
{
- action: copyWidget,
+ action: function (event, widget) {
+ copyWidget(event, layoutCtx, widget);
+ },
enabled: true,
value: "action.copy",
icon: "content_copy",
@@ -520,7 +680,20 @@ export default function DashboardController(types, widgetService, userService,
);
widgetContextActions.push(
{
- action: removeWidget,
+ action: function (event, widget) {
+ copyWidgetReference(event, layoutCtx, widget);
+ },
+ enabled: true,
+ value: "action.copy-reference",
+ icon: "content_copy",
+ shortcut: "M-R"
+ }
+ );
+ widgetContextActions.push(
+ {
+ action: function (event, widget) {
+ removeWidget(event, layoutCtx, widget);
+ },
enabled: true,
value: "action.delete",
icon: "clear",
@@ -532,8 +705,12 @@ export default function DashboardController(types, widgetService, userService,
return widgetContextActions;
}
- function copyWidget($event, widget) {
- itembuffer.copyWidget(vm.dashboard, widget);
+ function copyWidget($event, layoutCtx, widget) {
+ itembuffer.copyWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget);
+ }
+
+ function copyWidgetReference($event, layoutCtx, widget) {
+ itembuffer.copyWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget);
}
function helpLinkIdForWidgetType() {
@@ -562,36 +739,45 @@ export default function DashboardController(types, widgetService, userService,
}
function displayTitle() {
- if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
- angular.isDefined(vm.dashboard.configuration.gridSettings.showTitle)) {
- return vm.dashboard.configuration.gridSettings.showTitle;
+ if (vm.dashboard && vm.dashboard.configuration.settings &&
+ angular.isDefined(vm.dashboard.configuration.settings.showTitle)) {
+ return vm.dashboard.configuration.settings.showTitle;
} else {
return true;
}
}
function displayExport() {
- if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
- angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardExport)) {
- return vm.dashboard.configuration.gridSettings.showDashboardExport;
+ if (vm.dashboard && vm.dashboard.configuration.settings &&
+ angular.isDefined(vm.dashboard.configuration.settings.showDashboardExport)) {
+ return vm.dashboard.configuration.settings.showDashboardExport;
} else {
return true;
}
}
function displayDashboardTimewindow() {
- if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
- angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardTimewindow)) {
- return vm.dashboard.configuration.gridSettings.showDashboardTimewindow;
+ if (vm.dashboard && vm.dashboard.configuration.settings &&
+ angular.isDefined(vm.dashboard.configuration.settings.showDashboardTimewindow)) {
+ return vm.dashboard.configuration.settings.showDashboardTimewindow;
} else {
return true;
}
}
- function displayDevicesSelect() {
- if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
- angular.isDefined(vm.dashboard.configuration.gridSettings.showDevicesSelect)) {
- return vm.dashboard.configuration.gridSettings.showDevicesSelect;
+ function displayDashboardsSelect() {
+ if (vm.dashboard && vm.dashboard.configuration.settings &&
+ angular.isDefined(vm.dashboard.configuration.settings.showDashboardsSelect)) {
+ return vm.dashboard.configuration.settings.showDashboardsSelect;
+ } else {
+ return true;
+ }
+ }
+
+ function displayEntitiesSelect() {
+ if (vm.dashboard && vm.dashboard.configuration.settings &&
+ angular.isDefined(vm.dashboard.configuration.settings.showEntitiesSelect)) {
+ return vm.dashboard.configuration.settings.showEntitiesSelect;
} else {
return true;
}
@@ -601,32 +787,40 @@ export default function DashboardController(types, widgetService, userService,
if (widgetForm.$dirty) {
widgetForm.$setPristine();
vm.editingWidget = angular.copy(vm.editingWidgetOriginal);
+ vm.editingWidgetLayout = angular.copy(vm.editingWidgetLayoutOriginal);
}
}
function saveWidget(widgetForm) {
widgetForm.$setPristine();
var widget = angular.copy(vm.editingWidget);
- var index = vm.widgets.indexOf(vm.editingWidgetOriginal);
- vm.widgets[index] = widget;
+ var widgetLayout = angular.copy(vm.editingWidgetLayout);
+ var id = vm.editingWidgetOriginal.id;
+ var index = vm.editingLayoutCtx.widgets.indexOf(vm.editingWidgetOriginal);
+ vm.dashboardConfiguration.widgets[id] = widget;
vm.editingWidgetOriginal = widget;
- vm.dashboardContainer.highlightWidget(vm.editingWidgetOriginal, 0);
+ vm.editingWidgetLayoutOriginal = widgetLayout;
+ vm.editingLayoutCtx.widgets[index] = widget;
+ vm.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout;
+ vm.editingLayoutCtx.ctrl.highlightWidget(vm.editingWidgetOriginal, 0);
}
function onEditWidgetClosed() {
vm.editingWidgetOriginal = null;
vm.editingWidget = null;
+ vm.editingWidgetLayoutOriginal = null;
+ vm.editingWidgetLayout = null;
+ vm.editingLayoutCtx = null;
vm.editingWidgetSubtitle = null;
vm.isEditingWidget = false;
- if (vm.dashboardContainer) {
- vm.dashboardContainer.resetHighlight();
- }
+ resetHighlight();
vm.forceDashboardMobileMode = false;
}
- function addWidget() {
+ function addWidget(event, layoutCtx) {
loadWidgetLibrary();
vm.isAddingWidget = true;
+ vm.addingLayoutCtx = layoutCtx;
}
function onAddWidgetClosed() {
@@ -636,6 +830,33 @@ export default function DashboardController(types, widgetService, userService,
vm.staticWidgetTypes = [];
}
+ function selectTargetLayout($event) {
+ var deferred = $q.defer();
+ var layouts = vm.dashboardConfiguration.states[vm.dashboardCtx.state].layouts;
+ var layoutIds = Object.keys(layouts);
+ if (layoutIds.length > 1) {
+ $mdDialog.show({
+ controller: 'SelectTargetLayoutController',
+ controllerAs: 'vm',
+ templateUrl: selectTargetLayoutTemplate,
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: $event
+ }).then(
+ function success(layoutId) {
+ deferred.resolve(layoutId);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.resolve(layoutIds[0]);
+ }
+ return deferred.promise;
+ }
+
function addWidgetFromType(event, widget) {
vm.onAddWidgetClosed();
vm.isAddingWidget = false;
@@ -655,17 +876,22 @@ export default function DashboardController(types, widgetService, userService,
config: config
};
+ function addWidgetToLayout(widget, layoutId) {
+ dashboardUtils.addWidgetToLayout(vm.dashboard, vm.dashboardCtx.state, layoutId, widget);
+ vm.layouts[layoutId].layoutCtx.widgets.push(widget);
+ }
+
function addWidget(widget) {
- var columns = 24;
- if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
- columns = vm.dashboard.configuration.gridSettings.columns;
- }
- if (columns != 24) {
- var ratio = columns / 24;
- widget.sizeX *= ratio;
- widget.sizeY *= ratio;
+ if (vm.addingLayoutCtx) {
+ addWidgetToLayout(widget, vm.addingLayoutCtx.id);
+ vm.addingLayoutCtx = null;
+ } else {
+ selectTargetLayout(event).then(
+ function success(layoutId) {
+ addWidgetToLayout(widget, layoutId);
+ }
+ );
}
- vm.widgets.push(widget);
}
if (widgetTypeInfo.useCustomDatasources) {
@@ -677,7 +903,7 @@ export default function DashboardController(types, widgetService, userService,
templateUrl: addWidgetTemplate,
locals: {
dashboard: vm.dashboard,
- aliasesInfo: vm.aliasesInfo,
+ aliasesInfo: vm.dashboardCtx.aliasesInfo,
widget: newWidget,
widgetInfo: widgetTypeInfo
},
@@ -691,17 +917,17 @@ export default function DashboardController(types, widgetService, userService,
}
}).then(function (result) {
var widget = result.widget;
- vm.aliasesInfo = result.aliasesInfo;
+ vm.dashboardCtx.aliasesInfo = result.aliasesInfo;
addWidget(widget);
}, function (rejection) {
- vm.aliasesInfo = rejection.aliasesInfo;
+ vm.dashboardCtx.aliasesInfo = rejection.aliasesInfo;
});
}
}
);
}
- function removeWidget(event, widget) {
+ function removeWidget(event, layoutCtx, widget) {
var title = widget.config.title;
if (!title || title.length === 0) {
title = widgetService.getInstantWidgetInfo(widget).widgetName;
@@ -714,32 +940,61 @@ export default function DashboardController(types, widgetService, userService,
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
- vm.widgets.splice(vm.widgets.indexOf(widget), 1);
+ var index = layoutCtx.widgets.indexOf(widget);
+ if (index > -1) {
+ layoutCtx.widgets.splice(index, 1);
+ dashboardUtils.removeWidgetFromLayout(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, widget.id);
+ }
});
}
+ function pasteWidget(event, layoutCtx, pos) {
+ itembuffer.pasteWidget(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos, entityAliasesUpdated).then(
+ function (widget) {
+ if (widget) {
+ layoutCtx.widgets.push(widget);
+ }
+ }
+ );
+ }
+
+ function pasteWidgetReference(event, layoutCtx, pos) {
+ itembuffer.pasteWidgetReference(vm.dashboard, vm.dashboardCtx.state, layoutCtx.id, pos).then(
+ function (widget) {
+ if (widget) {
+ layoutCtx.widgets.push(widget);
+ }
+ }
+ );
+ }
+
function setEditMode(isEdit, revert) {
vm.isEdit = isEdit;
if (vm.isEdit) {
- if (vm.widgetEditMode) {
- vm.prevWidgets = angular.copy(vm.widgets);
- } else {
- vm.prevDashboard = angular.copy(vm.dashboard);
- }
+ vm.prevDashboard = angular.copy(vm.dashboard);
+ vm.prevDashboardState = vm.dashboardCtx.state;
} else {
if (vm.widgetEditMode) {
if (revert) {
- vm.widgets = vm.prevWidgets;
+ vm.dashboard = vm.prevDashboard;
}
} else {
- if (vm.dashboardContainer) {
- vm.dashboardContainer.resetHighlight();
- }
+ resetHighlight();
if (revert) {
vm.dashboard = vm.prevDashboard;
- vm.widgets = vm.dashboard.configuration.widgets;
vm.dashboardConfiguration = vm.dashboard.configuration;
- deviceAliasesUpdated();
+ openDashboardState(vm.prevDashboardState);
+ entityAliasesUpdated();
+ }
+ }
+ }
+ }
+
+ function resetHighlight() {
+ for (var l in vm.layouts) {
+ if (vm.layouts[l].layoutCtx) {
+ if (vm.layouts[l].layoutCtx.ctrl) {
+ vm.layouts[l].layoutCtx.ctrl.resetHighlight();
}
}
}
@@ -768,21 +1023,27 @@ export default function DashboardController(types, widgetService, userService,
$mdDialog.show(alert);
}
- function deviceAliasesUpdated() {
- deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
+ function entityAliasesUpdated() {
+ var deferred = $q.defer();
+ entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
.then(
function(resolution) {
if (resolution.aliasesInfo) {
- vm.aliasesInfo = resolution.aliasesInfo;
+ vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo;
}
+ deferred.resolve();
}
);
+ return deferred.promise;
}
function notifyDashboardUpdated() {
if (vm.widgetEditMode) {
var parentScope = $window.parent.angular.element($window.frameElement).scope();
- var widget = vm.widgets[0];
+ var widget = vm.layouts.main.layoutCtx.widgets[0];
+ var layout = vm.layouts.main.layoutCtx.widgetLayouts[widget.id];
+ widget.sizeX = layout.sizeX;
+ widget.sizeY = layout.sizeY;
parentScope.$root.$broadcast('widgetEditUpdated', widget);
parentScope.$root.$apply();
} else {
diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js
index e9fe1f2..92bb362 100644
--- a/ui/src/app/dashboard/dashboard.routes.js
+++ b/ui/src/app/dashboard/dashboard.routes.js
@@ -66,7 +66,8 @@ export default function DashboardRoutes($stateProvider) {
}
})
.state('home.dashboards.dashboard', {
- url: '/:dashboardId',
+ url: '/:dashboardId?state',
+ reloadOnSearch: false,
module: 'private',
auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
views: {
@@ -86,7 +87,8 @@ export default function DashboardRoutes($stateProvider) {
}
})
.state('home.customers.dashboards.dashboard', {
- url: '/:dashboardId',
+ url: '/:dashboardId?state',
+ reloadOnSearch: false,
module: 'private',
auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
views: {
ui/src/app/dashboard/dashboard.scss 36(+35 -1)
diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss
index 8f50ca2..bc5ec56 100644
--- a/ui/src/app/dashboard/dashboard.scss
+++ b/ui/src/app/dashboard/dashboard.scss
@@ -63,7 +63,7 @@ tb-details-sidenav.tb-widget-details-sidenav {
section.tb-dashboard-toolbar {
position: absolute;
top: 0px;
- left: -100%;
+ left: 0px;
z-index: 3;
pointer-events: none;
&.tb-dashboard-toolbar-opened {
@@ -118,6 +118,27 @@ section.tb-dashboard-toolbar {
.close-action {
margin-right: -18px;
}
+ .md-fab-action-item {
+ width: 100%;
+ height: 46px;
+ .tb-dashboard-action-panels {
+ height: 46px;
+ flex-direction: row-reverse;
+ .tb-dashboard-action-panel {
+ height: 46px;
+ flex-direction: row-reverse;
+ div {
+ height: 46px;
+ }
+ md-select {
+ pointer-events: all;
+ }
+ tb-states-component {
+ pointer-events: all;
+ }
+ }
+ }
+ }
}
}
}
@@ -133,6 +154,19 @@ section.tb-dashboard-toolbar {
margin-top: 0px;
@include transition(margin-top .3s cubic-bezier(.55,0,.55,.2) .2s);
}
+ .tb-dashboard-layouts {
+ md-backdrop {
+ z-index: 1;
+ }
+ #tb-main-layout {
+
+ }
+ #tb-right-layout {
+ md-sidenav {
+ z-index: 1;
+ }
+ }
+ }
}
/*****************************
ui/src/app/dashboard/dashboard.tpl.html 241(+130 -111)
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 9276e0f..078daab 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -16,16 +16,10 @@
-->
<md-content flex tb-expand-fullscreen="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-button-id="dashboard-expand-button"
- hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom"
- ng-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
- 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
- 'background-repeat': 'no-repeat',
- 'background-attachment': 'scroll',
- 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
- 'background-position': '0% 0%'}">
+ hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom">
<section class="tb-dashboard-toolbar" ng-show="vm.showDashboardToolbar()"
ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
- <md-fab-toolbar md-open="vm.toolbarOpened"
+ <md-fab-toolbar ng-show="!vm.widgetEditMode" md-open="vm.toolbarOpened"
md-direction="left">
<md-fab-trigger class="align-with-text">
<md-button aria-label="menu" class="md-fab md-primary" ng-click="vm.openToolbar()">
@@ -37,77 +31,100 @@
</md-fab-trigger>
<md-toolbar>
<md-fab-actions class="md-toolbar-tools">
- <md-button ng-show="!vm.isEdit" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
- <md-tooltip md-direction="bottom">
- {{ 'dashboard.close-toolbar' | translate }}
- </md-tooltip>
- <md-icon aria-label="close-toolbar" class="material-icons">arrow_forward</md-icon>
- </md-button>
- <md-button id="dashboard-expand-button"
- aria-label="{{ 'fullscreen.fullscreen' | translate }}"
- class="md-icon-button">
- </md-button>
- <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
- </tb-user-menu>
- <md-button ng-show="vm.isEdit || vm.displayExport()"
- aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
- ng-click="vm.exportDashboard($event)">
- <md-tooltip md-direction="bottom">
- {{ 'dashboard.export' | translate }}
- </md-tooltip>
- <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
- </md-button>
- <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
- is-toolbar
- direction="left"
- tooltip-direction="bottom" aggregation
- ng-model="vm.dashboardConfiguration.timewindow">
- </tb-timewindow>
- <tb-aliases-device-select ng-show="!vm.isEdit && vm.displayDevicesSelect()"
- tooltip-direction="bottom"
- ng-model="vm.aliasesInfo.deviceAliases"
- device-aliases-info="vm.aliasesInfo.deviceAliasesInfo">
- </tb-aliases-device-select>
- <md-button ng-show="vm.isEdit" aria-label="{{ 'device.aliases' | translate }}" class="md-icon-button"
- ng-click="vm.openDeviceAliases($event)">
- <md-tooltip md-direction="bottom">
- {{ 'device.aliases' | translate }}
- </md-tooltip>
- <md-icon aria-label="{{ 'device.aliases' | translate }}" class="material-icons">devices_other</md-icon>
- </md-button>
- <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
- ng-click="vm.openDashboardSettings($event)">
- <md-tooltip md-direction="bottom">
- {{ 'dashboard.settings' | translate }}
- </md-tooltip>
- <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
- </md-button>
- <tb-dashboard-select ng-show="!vm.isEdit && !vm.widgetEditMode"
- ng-model="vm.currentDashboardId"
- dashboards-scope="{{vm.currentDashboardScope}}"
- customer-id="vm.currentCustomerId">
- </tb-dashboard-select>
+ <div class="tb-dashboard-action-panels" flex layout="row" layout-align="start center">
+ <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="start center">
+ <md-button ng-show="vm.showCloseToolbar()" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
+ <md-tooltip md-direction="bottom">
+ {{ 'dashboard.close-toolbar' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="close-toolbar" class="material-icons">arrow_forward</md-icon>
+ </md-button>
+ <md-button ng-show="vm.showRightLayoutSwitch()" aria-label="switch-layouts" class="md-icon-button" ng-click="vm.toggleLayouts()">
+ <ng-md-icon icon="{{vm.isRightLayoutOpened ? 'arrow_back' : 'menu'}}" options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+ <md-tooltip md-direction="bottom">
+ {{ (vm.isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button id="dashboard-expand-button"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button">
+ </md-button>
+ <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
+ </tb-user-menu>
+ <md-button ng-show="vm.isEdit || vm.displayExport()"
+ aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
+ ng-click="vm.exportDashboard($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'dashboard.export' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
+ </md-button>
+ <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
+ is-toolbar
+ direction="left"
+ tooltip-direction="bottom" aggregation
+ ng-model="vm.dashboardConfiguration.timewindow">
+ </tb-timewindow>
+ <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
+ tooltip-direction="bottom"
+ ng-model="vm.dashboardCtx.aliasesInfo.entityAliases"
+ entity-aliases-info="vm.dashboardCtx.aliasesInfo.entityAliasesInfo">
+ </tb-aliases-entity-select>
+ <md-button ng-show="vm.isEdit" aria-label="{{ 'entity.aliases' | translate }}" class="md-icon-button"
+ ng-click="vm.openEntityAliases($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'entity.aliases' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'entity.aliases' | translate }}" class="material-icons">devices_other</md-icon>
+ </md-button>
+ <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
+ ng-click="vm.openDashboardSettings($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'dashboard.settings' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
+ </md-button>
+ <tb-dashboard-select ng-show="!vm.isEdit && !vm.widgetEditMode && vm.displayDashboardsSelect()"
+ ng-model="vm.currentDashboardId"
+ dashboards-scope="{{vm.currentDashboardScope}}"
+ customer-id="vm.currentCustomerId">
+ </tb-dashboard-select>
+ </div>
+ <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="end center">
+ <div layout="row" layout-align="start center" ng-show="vm.isEdit">
+ <md-button aria-label="{{ 'dashboard.manage-states' | translate }}" class="md-icon-button"
+ ng-click="vm.manageDashboardStates($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'dashboard.manage-states' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'dashboard.manage-states' | translate }}" class="material-icons">layers</md-icon>
+ </md-button>
+ <md-button aria-label="{{ 'layout.manage' | translate }}" class="md-icon-button"
+ ng-click="vm.manageDashboardLayouts($event)">
+ <md-tooltip md-direction="bottom">
+ {{ 'layout.manage' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'layout.manage' | translate }}" class="material-icons">view_compact</md-icon>
+ </md-button>
+ </div>
+ <div layout="row" layout-align="start center">
+ <tb-states-component ng-if="vm.isEdit" states-controller-id="'default'"
+ dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
+ </tb-states-component>
+ <tb-states-component ng-if="!vm.isEdit" states-controller-id="vm.dashboardConfiguration.settings.stateControllerId"
+ dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
+ </tb-states-component>
+ </div>
+ </div>
+ </div>
</md-fab-actions>
</md-toolbar>
</md-fab-toolbar>
</section>
<section class="tb-dashboard-container tb-absolute-fill"
ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
- <section ng-show="!loading && vm.noData()" layout-align="center center"
- ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
- ng-class="{'tb-padded' : !vm.widgetEditMode}"
- style="text-transform: uppercase; display: flex; z-index: 1;"
- class="md-headline tb-absolute-fill">
- <span translate ng-if="!vm.isEdit">
- dashboard.no-widgets
- </span>
- <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget($event)">
- <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
- {{ 'dashboard.add-widget' | translate }}
- </md-button>
- </section>
<section ng-show="!loading && vm.dashboardConfigurationError()" layout-align="center center"
- ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"
+ ng-style="{'color': vm.dashboard.configuration.settings.titleColor}"
ng-class="{'tb-padded' : !vm.widgetEditMode}"
style="text-transform: uppercase; display: flex; z-index: 1;"
class="md-headline tb-absolute-fill">
@@ -116,46 +133,47 @@
</span>
</section>
<section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center"
- ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
+ ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">
<h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
<md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">
- <label translate ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">dashboard.title</label>
- <input class="tb-dashboard-title" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}" required name="title" ng-model="vm.dashboard.title">
+ <label translate ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">dashboard.title</label>
+ <input class="tb-dashboard-title" ng-style="{'color': vm.dashboard.configuration.settings.titleColor}" required name="title" ng-model="vm.dashboard.title">
</md-input-container>
</section>
- <div class="tb-absolute-fill"
+ <div class="tb-absolute-fill tb-dashboard-layouts" layout="{{vm.forceDashboardMobileMode ? 'column' : 'row'}}"
ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }">
- <tb-dashboard
- dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
- 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
- 'background-repeat': 'no-repeat',
- 'background-attachment': 'scroll',
- 'background-size': vm.dashboard.configuration.gridSettings.backgroundSizeMode || '100%',
- 'background-position': '0% 0%'}"
- widgets="vm.widgets"
- columns="vm.dashboard.configuration.gridSettings.columns"
- margins="vm.dashboard.configuration.gridSettings.margins"
- aliases-info="vm.aliasesInfo"
- dashboard-timewindow="vm.dashboardConfiguration.timewindow"
- is-edit="vm.isEdit"
- is-mobile="vm.forceDashboardMobileMode"
- is-mobile-disabled="vm.widgetEditMode"
- is-edit-action-enabled="vm.isEdit"
- is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
- is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
- on-edit-widget="vm.editWidget(event, widget)"
- on-export-widget="vm.exportWidget(event, widget)"
- on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
- on-widget-clicked="vm.widgetClicked(event, widget)"
- on-widget-context-menu="vm.widgetContextMenu(event, widget)"
- prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
- prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
- on-remove-widget="vm.removeWidget(event, widget)"
- load-widgets="vm.loadDashboard()"
- get-st-diff="vm.getServerTimeDiff()"
- on-init="vm.dashboardInited(dashboard)"
- on-init-failed="vm.dashboardInitFailed(e)">
- </tb-dashboard>
+ <div ng-show="vm.layouts.main.show"
+ id="tb-main-layout"
+ ng-style="{width: vm.mainLayoutWidth(),
+ height: vm.mainLayoutHeight()}">
+ <tb-dashboard-layout layout-ctx="vm.layouts.main.layoutCtx"
+ dashboard-ctx="vm.dashboardCtx"
+ is-edit="vm.isEdit"
+ is-mobile="vm.forceDashboardMobileMode"
+ widget-edit-mode="vm.widgetEditMode"
+ get-st-diff="vm.getServerTimeDiff()">
+ </tb-dashboard-layout>
+ </div>
+ <md-sidenav ng-if="vm.layouts.right.show"
+ id="tb-right-layout"
+ class="md-sidenav-right"
+ ng-style="{minWidth: vm.rightLayoutWidth(),
+ maxWidth: vm.rightLayoutWidth(),
+ height: vm.rightLayoutHeight(),
+ zIndex: 1}"
+ md-component-id="right-dashboard-layout"
+ aria-label="Right dashboard layout"
+ md-is-open="!vm.isMobile || vm.isRightLayoutOpened"
+ md-is-locked-open="!vm.isMobile">
+ <tb-dashboard-layout style="height: 100%;"
+ layout-ctx="vm.layouts.right.layoutCtx"
+ dashboard-ctx="vm.dashboardCtx"
+ is-edit="vm.isEdit"
+ is-mobile="vm.forceDashboardMobileMode"
+ widget-edit-mode="vm.widgetEditMode"
+ get-st-diff="vm.getServerTimeDiff()">
+ </tb-dashboard-layout>
+ </md-sidenav>
</div>
<tb-details-sidenav class="tb-widget-details-sidenav"
header-title="{{vm.editingWidget.config.title}}"
@@ -173,8 +191,9 @@
<form name="vm.widgetForm" ng-if="vm.isEditingWidget">
<tb-edit-widget
dashboard="vm.dashboard"
- aliases-info="vm.aliasesInfo"
+ aliases-info="vm.dashboardCtx.aliasesInfo"
widget="vm.editingWidget"
+ widget-layout="vm.editingWidgetLayout"
the-form="vm.widgetForm">
</tb-edit-widget>
</form>
@@ -286,7 +305,7 @@
</md-button>
</md-fab-actions>
</md-fab-speed-dial>
- <md-button ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading" ng-disabled="loading"
+ <md-button ng-if="(vm.isTenantAdmin() || vm.isSystemAdmin()) && !forceFullscreen" ng-show="vm.isEdit && !vm.isAddingWidget && !loading" ng-disabled="loading"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}"
ng-click="vm.saveDashboard()">
@@ -296,7 +315,7 @@
<ng-md-icon icon="done"></ng-md-icon>
</md-button>
<md-button ng-show="!vm.isAddingWidget && !loading"
- ng-if="vm.isTenantAdmin() || vm.isSystemAdmin()" ng-disabled="loading"
+ ng-if="(vm.isTenantAdmin() || vm.isSystemAdmin()) && !forceFullscreen" ng-disabled="loading"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.edit-mode' | translate }}"
ng-click="vm.toggleDashboardEditMode()">
@@ -308,7 +327,7 @@
</md-button>
</section>
</section>
- <section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
+ <section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.settings.titleColor}">
<span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ vm.thingsboardVersion }}</a></span>
</section>
</md-content>
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
index 06c64eb..dbe4cbe 100644
--- a/ui/src/app/dashboard/dashboard-settings.controller.js
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -16,7 +16,7 @@
import './dashboard-settings.scss';
/*@ngInject*/
-export default function DashboardSettingsController($scope, $mdDialog, gridSettings) {
+export default function DashboardSettingsController($scope, $mdDialog, statesControllerService, settings, gridSettings) {
var vm = this;
@@ -25,32 +25,49 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
vm.imageAdded = imageAdded;
vm.clearImage = clearImage;
- vm.gridSettings = gridSettings || {};
+ vm.settings = settings;
+ vm.gridSettings = gridSettings;
+ vm.stateControllers = statesControllerService.getStateControllers();
- if (angular.isUndefined(vm.gridSettings.showTitle)) {
- vm.gridSettings.showTitle = true;
- }
+ if (vm.settings) {
+ if (angular.isUndefined(vm.settings.stateControllerId)) {
+ vm.settings.stateControllerId = 'default';
+ }
- if (angular.isUndefined(vm.gridSettings.showDevicesSelect)) {
- vm.gridSettings.showDevicesSelect = true;
- }
+ if (angular.isUndefined(vm.settings.showTitle)) {
+ vm.settings.showTitle = true;
+ }
- if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) {
- vm.gridSettings.showDashboardTimewindow = true;
- }
+ if (angular.isUndefined(vm.settings.titleColor)) {
+ vm.settings.titleColor = 'rgba(0,0,0,0.870588)';
+ }
- if (angular.isUndefined(vm.gridSettings.showDashboardExport)) {
- vm.gridSettings.showDashboardExport = true;
- }
+ if (angular.isUndefined(vm.settings.showDashboardsSelect)) {
+ vm.settings.showDashboardsSelect = true;
+ }
- vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
- vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)';
- vm.gridSettings.columns = vm.gridSettings.columns || 24;
- vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
- vm.hMargin = vm.gridSettings.margins[0];
- vm.vMargin = vm.gridSettings.margins[1];
+ if (angular.isUndefined(vm.settings.showEntitiesSelect)) {
+ vm.settings.showEntitiesSelect = true;
+ }
- vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%';
+ if (angular.isUndefined(vm.settings.showDashboardTimewindow)) {
+ vm.settings.showDashboardTimewindow = true;
+ }
+
+ if (angular.isUndefined(vm.settings.showDashboardExport)) {
+ vm.settings.showDashboardExport = true;
+ }
+ }
+
+ if (vm.gridSettings) {
+ vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
+ vm.gridSettings.color = vm.gridSettings.color || 'rgba(0,0,0,0.870588)';
+ vm.gridSettings.columns = vm.gridSettings.columns || 24;
+ vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
+ vm.hMargin = vm.gridSettings.margins[0];
+ vm.vMargin = vm.gridSettings.margins[1];
+ vm.gridSettings.backgroundSizeMode = vm.gridSettings.backgroundSizeMode || '100%';
+ }
function cancel() {
$mdDialog.cancel();
@@ -76,7 +93,14 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
function save() {
$scope.theForm.$setPristine();
- vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
- $mdDialog.hide(vm.gridSettings);
+ if (vm.gridSettings) {
+ vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
+ }
+ $mdDialog.hide(
+ {
+ settings: vm.settings,
+ gridSettings: vm.gridSettings
+ }
+ );
}
}
ui/src/app/dashboard/dashboard-settings.tpl.html 215(+121 -94)
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
index ec6f28b..88fc66d 100644
--- a/ui/src/app/dashboard/dashboard-settings.tpl.html
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -19,7 +19,7 @@
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
- <h2 translate>dashboard.settings</h2>
+ <h2 translate>{{vm.settings ? 'dashboard.settings' : 'layout.settings'}}</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
@@ -31,15 +31,53 @@
<md-dialog-content>
<div class="md-dialog-content">
<fieldset ng-disabled="loading">
- <div layout="row" layout-align="start center">
- <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
- ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }}
- </md-checkbox>
+ <div ng-show="vm.settings">
+ <md-input-container class="md-block">
+ <label translate>dashboard.state-controller</label>
+ <md-select aria-label="{{ 'dashboard.state-controller' | translate }}" ng-model="vm.settings.stateControllerId">
+ <md-option ng-repeat="(stateControllerId, stateController) in vm.stateControllers" ng-value="stateControllerId">
+ {{stateControllerId}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <div layout="row" layout-align="start center">
+ <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
+ ng-model="vm.settings.showTitle">{{ 'dashboard.display-title' | translate }}
+ </md-checkbox>
+ <div flex
+ ng-required="false"
+ md-color-picker
+ ng-model="vm.settings.titleColor"
+ label="{{ 'dashboard.title-color' | translate }}"
+ icon="format_color_fill"
+ default="rgba(0, 0, 0, 0.870588)"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false"
+ ></div>
+ </div>
+ <div layout="row" layout-align="start center">
+ <md-checkbox flex aria-label="{{ 'dashboard.display-dashboards-selection' | translate }}"
+ ng-model="vm.settings.showDashboardsSelect">{{ 'dashboard.display-dashboards-selection' | translate }}
+ </md-checkbox>
+ <md-checkbox flex aria-label="{{ 'dashboard.display-entities-selection' | translate }}"
+ ng-model="vm.settings.showEntitiesSelect">{{ 'dashboard.display-entities-selection' | translate }}
+ </md-checkbox>
+ <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-timewindow' | translate }}"
+ ng-model="vm.settings.showDashboardTimewindow">{{ 'dashboard.display-dashboard-timewindow' | translate }}
+ </md-checkbox>
+ <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-export' | translate }}"
+ ng-model="vm.settings.showDashboardExport">{{ 'dashboard.display-dashboard-export' | translate }}
+ </md-checkbox>
+ </div>
+ </div>
+ <div ng-show="vm.gridSettings">
<div flex
ng-required="false"
md-color-picker
- ng-model="vm.gridSettings.titleColor"
- label="{{ 'dashboard.title-color' | translate }}"
+ ng-model="vm.gridSettings.color"
+ label="{{ 'layout.color' | translate }}"
icon="format_color_fill"
default="rgba(0, 0, 0, 0.870588)"
md-color-clear-button="false"
@@ -47,98 +85,87 @@
md-color-generic-palette="false"
md-color-history="false"
></div>
- </div>
- <div layout="row" layout-align="start center">
- <md-checkbox flex aria-label="{{ 'dashboard.display-device-selection' | translate }}"
- ng-model="vm.gridSettings.showDevicesSelect">{{ 'dashboard.display-device-selection' | translate }}
- </md-checkbox>
- <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-timewindow' | translate }}"
- ng-model="vm.gridSettings.showDashboardTimewindow">{{ 'dashboard.display-dashboard-timewindow' | translate }}
- </md-checkbox>
- <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-export' | translate }}"
- ng-model="vm.gridSettings.showDashboardExport">{{ 'dashboard.display-dashboard-export' | translate }}
- </md-checkbox>
- </div>
- <md-input-container class="md-block">
- <label translate>dashboard.columns-count</label>
- <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
- max="1000" />
- <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
- <div ng-message="required" translate>dashboard.columns-count-required</div>
- <div ng-message="min" translate>dashboard.min-columns-count-message</div>
- <div ng-message="max">dashboard.max-columns-count-message</div>
- </div>
- </md-input-container>
- <small translate>dashboard.widgets-margins</small>
- <div flex layout="row">
- <md-input-container flex class="md-block">
- <label translate>dashboard.horizontal-margin</label>
- <input required type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
- max="50" />
- <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
- <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
- <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
- <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
- </div>
- </md-input-container>
- <md-input-container flex class="md-block">
- <label translate>dashboard.vertical-margin</label>
- <input required type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
- max="50" />
- <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
- <div ng-message="required" translate>dashboard.vertical-margin-required</div>
- <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
- <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+ <md-input-container class="md-block">
+ <label translate>dashboard.columns-count</label>
+ <input ng-required="vm.gridSettings" type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
+ max="1000" />
+ <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.columns-count-required</div>
+ <div ng-message="min" translate>dashboard.min-columns-count-message</div>
+ <div ng-message="max">dashboard.max-columns-count-message</div>
</div>
</md-input-container>
- </div>
- <div flex
- ng-required="false"
- md-color-picker
- ng-model="vm.gridSettings.backgroundColor"
- label="{{ 'dashboard.background-color' | translate }}"
- icon="format_color_fill"
- default="rgba(0,0,0,0)"
- md-color-clear-button="false"
- open-on-input="true"
- md-color-generic-palette="false"
- md-color-history="false"
- ></div>
- <div class="tb-container">
- <label class="tb-label" translate>dashboard.background-image</label>
- <div flow-init="{singleFile:true}"
- flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
- <div class="tb-image-preview-container">
- <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
- <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
- </div>
- <div class="tb-image-clear-container">
- <md-button ng-click="vm.clearImage()"
- class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'action.remove' | translate }}
- </md-tooltip>
- <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
- close
- </md-icon>
- </md-button>
- </div>
- <div class="alert tb-flow-drop" flow-drop>
- <label for="select" translate>dashboard.drop-image</label>
- <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+ <small translate>dashboard.widgets-margins</small>
+ <div flex layout="row">
+ <md-input-container flex class="md-block">
+ <label translate>dashboard.horizontal-margin</label>
+ <input ng-required="vm.gridSettings" type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
+ max="50" />
+ <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
+ <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
+ <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>dashboard.vertical-margin</label>
+ <input ng-required="vm.gridSettings" type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
+ max="50" />
+ <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.vertical-margin-required</div>
+ <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
+ <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+ </div>
+ </md-input-container>
+ </div>
+ <div flex
+ ng-required="false"
+ md-color-picker
+ ng-model="vm.gridSettings.backgroundColor"
+ label="{{ 'dashboard.background-color' | translate }}"
+ icon="format_color_fill"
+ default="rgba(0,0,0,0)"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false"
+ ></div>
+ <div class="tb-container">
+ <label class="tb-label" translate>dashboard.background-image</label>
+ <div flow-init="{singleFile:true}"
+ flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
+ <div class="tb-image-preview-container">
+ <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
+ <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
+ </div>
+ <div class="tb-image-clear-container">
+ <md-button ng-click="vm.clearImage()"
+ class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="select" translate>dashboard.drop-image</label>
+ <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+ </div>
</div>
</div>
+ <md-input-container class="md-block">
+ <label translate>dashboard.background-size-mode</label>
+ <md-select ng-model="vm.gridSettings.backgroundSizeMode" placeholder="{{ 'dashboard.background-size-mode' | translate }}">
+ <md-option value="100%">Fit width</md-option>
+ <md-option value="auto 100%">Fit height</md-option>
+ <md-option value="cover">Cover</md-option>
+ <md-option value="contain">Contain</md-option>
+ <md-option value="auto">Original size</md-option>
+ </md-select>
+ </md-input-container>
</div>
- <md-input-container class="md-block">
- <label translate>dashboard.background-size-mode</label>
- <md-select ng-model="vm.gridSettings.backgroundSizeMode" placeholder="{{ 'dashboard.background-size-mode' | translate }}">
- <md-option value="100%">Fit width</md-option>
- <md-option value="auto 100%">Fit height</md-option>
- <md-option value="cover">Cover</md-option>
- <md-option value="contain">Contain</md-option>
- <md-option value="auto">Original size</md-option>
- </md-select>
- </md-input-container>
</fieldset>
</div>
</md-dialog-content>
ui/src/app/dashboard/edit-widget.directive.js 49(+30 -19)
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index ced39f5..7cba4ee 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);
@@ -34,7 +34,10 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
scope.widget.isSystemType).then(
function(widgetInfo) {
scope.$applyAsync(function(scope) {
- scope.widgetConfig = scope.widget.config;
+ scope.widgetConfig = {
+ config: scope.widget.config,
+ layout: scope.widgetLayout
+ };
var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
scope.isDataEnabled = !widgetInfo.useCustomDatasources;
@@ -58,45 +61,52 @@ 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.$watch('widgetLayout', function () {
+ if (scope.widgetLayout && scope.widgetConfig) {
+ scope.widgetConfig.layout = scope.widgetLayout;
+ }
+ });
+
+ 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 () {
@@ -116,6 +126,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
dashboard: '=',
aliasesInfo: '=',
widget: '=',
+ widgetLayout: '=',
theForm: '='
}
};
diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html
index 1aaf861..279d311 100644
--- a/ui/src/app/dashboard/edit-widget.tpl.html
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -21,9 +21,9 @@
is-data-enabled="isDataEnabled"
widget-settings-schema="settingsSchema"
datakey-settings-schema="dataKeySettingsSchema"
- device-aliases="aliasesInfo.deviceAliases"
+ entity-aliases="aliasesInfo.entityAliases"
functions-only="functionsOnly"
- fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)"
- on-create-device-alias="createDeviceAlias(event, alias)"
+ fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
+ on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"
the-form="theForm"></tb-widget-config>
</fieldset>
ui/src/app/dashboard/index.js 18(+7 -11)
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index 88d71fb..73a97a1 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -16,7 +16,6 @@
import './dashboard.scss';
import uiRouter from 'angular-ui-router';
-import gridster from 'angular-gridster';
import thingsboardGrid from '../components/grid.directive';
import thingsboardApiWidget from '../api/widget.service';
@@ -24,9 +23,9 @@ 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 thingsboardRelatedEntityAutocomplete from '../components/related-entity-autocomplete.directive';
import thingsboardDashboard from '../components/dashboard.directive';
import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
@@ -34,23 +33,21 @@ import thingsboardSocialsharePanel from '../components/socialshare-panel.directi
import thingsboardTypes from '../common/types.constant';
import thingsboardItemBuffer from '../services/item-buffer.service';
import thingsboardImportExport from '../import-export';
+import dashboardLayouts from './layouts';
+import dashboardStates from './states';
import DashboardRoutes from './dashboard.routes';
import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
import DashboardController from './dashboard.controller';
-import DeviceAliasesController from './device-aliases.controller';
-import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
import DashboardSettingsController from './dashboard-settings.controller';
import AssignDashboardToCustomerController from './assign-to-customer.controller';
import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
import AddWidgetController from './add-widget.controller';
import DashboardDirective from './dashboard.directive';
import EditWidgetDirective from './edit-widget.directive';
-import AliasesDeviceSelectDirective from './aliases-device-select.directive';
export default angular.module('thingsboard.dashboard', [
uiRouter,
- gridster.name,
thingsboardTypes,
thingsboardItemBuffer,
thingsboardImportExport,
@@ -60,26 +57,25 @@ export default angular.module('thingsboard.dashboard', [
thingsboardApiDashboard,
thingsboardApiCustomer,
thingsboardDetailsSidenav,
- thingsboardDeviceFilter,
thingsboardWidgetConfig,
thingsboardDashboardSelect,
+ thingsboardRelatedEntityAutocomplete,
thingsboardDashboard,
thingsboardExpandFullscreen,
thingsboardWidgetsBundleSelect,
- thingsboardSocialsharePanel
+ thingsboardSocialsharePanel,
+ dashboardLayouts,
+ dashboardStates
])
.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/layouts/dashboard-layout.directive.js b/ui/src/app/dashboard/layouts/dashboard-layout.directive.js
new file mode 100644
index 0000000..18926c3
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/dashboard-layout.directive.js
@@ -0,0 +1,277 @@
+/*
+ * 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 dashboardLayoutTemplate from './dashboard-layout.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardLayout() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ layoutCtx: '=',
+ dashboardCtx: '=',
+ isEdit: '=',
+ isMobile: '=',
+ widgetEditMode: '=',
+ getStDiff: '&?'
+ },
+ controller: DashboardLayoutController,
+ controllerAs: 'vm',
+ templateUrl: dashboardLayoutTemplate
+ };
+}
+
+/*@ngInject*/
+function DashboardLayoutController($scope, $rootScope, $translate, $window, hotkeys, itembuffer) {
+
+ var vm = this;
+
+ vm.noData = noData;
+ vm.addWidget = addWidget;
+ vm.editWidget = editWidget;
+ vm.exportWidget = exportWidget;
+ vm.widgetMouseDown = widgetMouseDown;
+ vm.widgetClicked = widgetClicked;
+ vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
+ vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
+ vm.removeWidget = removeWidget;
+ vm.pasteWidget = pasteWidget;
+ vm.pasteWidgetReference = pasteWidgetReference;
+ vm.dashboardInited = dashboardInited;
+ vm.dashboardInitFailed = dashboardInitFailed;
+
+ vm.reload = function() {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.reload();
+ }
+ };
+
+ vm.setResizing = function(resizing) {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.isResizing = resizing;
+ }
+ }
+
+ vm.resetHighlight = function() {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.resetHighlight();
+ }
+ };
+
+ vm.highlightWidget = function(widget, delay) {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.highlightWidget(widget, delay);
+ }
+ };
+
+ vm.selectWidget = function(widget, delay) {
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.selectWidget(widget, delay);
+ }
+ };
+
+ vm.dashboardInitComplete = false;
+
+ initHotKeys();
+
+ $scope.$on('$destroy', function() {
+ vm.dashboardContainer = null;
+ });
+
+ $scope.$watch('vm.layoutCtx', function () {
+ if (vm.layoutCtx) {
+ vm.layoutCtx.ctrl = vm;
+ }
+ });
+
+ function noData() {
+ return vm.dashboardInitComplete && vm.layoutCtx &&
+ vm.layoutCtx.widgets && vm.layoutCtx.widgets.length == 0;
+ }
+
+ function addWidget($event) {
+ if (vm.dashboardCtx.onAddWidget) {
+ vm.dashboardCtx.onAddWidget($event, vm.layoutCtx);
+ }
+ }
+
+ function editWidget($event, widget) {
+ if (vm.dashboardCtx.onEditWidget) {
+ vm.dashboardCtx.onEditWidget($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function exportWidget($event, widget) {
+ if (vm.dashboardCtx.onExportWidget) {
+ vm.dashboardCtx.onExportWidget($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function widgetMouseDown($event, widget) {
+ if (vm.dashboardCtx.onWidgetMouseDown) {
+ vm.dashboardCtx.onWidgetMouseDown($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function widgetClicked($event, widget) {
+ if (vm.dashboardCtx.onWidgetClicked) {
+ vm.dashboardCtx.onWidgetClicked($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function prepareDashboardContextMenu() {
+ if (vm.dashboardCtx.prepareDashboardContextMenu) {
+ return vm.dashboardCtx.prepareDashboardContextMenu(vm.layoutCtx);
+ }
+ }
+
+ function prepareWidgetContextMenu(widget) {
+ if (vm.dashboardCtx.prepareWidgetContextMenu) {
+ return vm.dashboardCtx.prepareWidgetContextMenu(vm.layoutCtx, widget);
+ }
+ }
+
+ function removeWidget($event, widget) {
+ if (vm.dashboardCtx.onRemoveWidget) {
+ vm.dashboardCtx.onRemoveWidget($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function dashboardInitFailed() {
+ var parentScope = $window.parent.angular.element($window.frameElement).scope();
+ parentScope.$emit('widgetEditModeInited');
+ parentScope.$apply();
+ vm.dashboardInitComplete = true;
+ }
+
+ function dashboardInited(dashboardContainer) {
+ vm.dashboardContainer = dashboardContainer;
+ vm.dashboardInitComplete = true;
+ }
+
+ function isHotKeyAllowed(event) {
+ var target = event.target || event.srcElement;
+ var scope = angular.element(target).scope();
+ return scope && scope.$parent !== $rootScope;
+ }
+
+ function initHotKeys() {
+ $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) {
+ hotkeys.bindTo($scope)
+ .add({
+ combo: 'ctrl+c',
+ description: translations['action.copy'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ var widget = vm.dashboardContainer.getSelectedWidget();
+ if (widget) {
+ event.preventDefault();
+ copyWidget(event, widget);
+ }
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+r',
+ description: translations['action.copy-reference'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ var widget = vm.dashboardContainer.getSelectedWidget();
+ if (widget) {
+ event.preventDefault();
+ copyWidgetReference(event, widget);
+ }
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+v',
+ description: translations['action.paste'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ if (itembuffer.hasWidget()) {
+ event.preventDefault();
+ pasteWidget(event);
+ }
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+i',
+ description: translations['action.paste-reference'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ if (itembuffer.canPasteWidgetReference(vm.dashboardCtx.dashboard,
+ vm.dashboardCtx.state, vm.layoutCtx.id)) {
+ event.preventDefault();
+ pasteWidgetReference(event);
+ }
+ }
+ }
+ })
+
+ .add({
+ combo: 'ctrl+x',
+ description: translations['action.delete'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ var widget = vm.dashboardContainer.getSelectedWidget();
+ if (widget) {
+ event.preventDefault();
+ vm.dashboardCtx.onRemoveWidget(event, vm.layoutCtx, widget);
+ }
+ }
+ }
+ });
+ });
+ }
+
+ function copyWidget($event, widget) {
+ if (vm.dashboardCtx.copyWidget) {
+ vm.dashboardCtx.copyWidget($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function copyWidgetReference($event, widget) {
+ if (vm.dashboardCtx.copyWidgetReference) {
+ vm.dashboardCtx.copyWidgetReference($event, vm.layoutCtx, widget);
+ }
+ }
+
+ function pasteWidget($event) {
+ var pos = vm.dashboardContainer.getEventGridPosition($event);
+ if (vm.dashboardCtx.pasteWidget) {
+ vm.dashboardCtx.pasteWidget($event, vm.layoutCtx, pos);
+ }
+ }
+
+ function pasteWidgetReference($event) {
+ var pos = vm.dashboardContainer.getEventGridPosition($event);
+ if (vm.dashboardCtx.pasteWidgetReference) {
+ vm.dashboardCtx.pasteWidgetReference($event, vm.layoutCtx, pos);
+ }
+ }
+
+}
diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
new file mode 100644
index 0000000..ea84858
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
@@ -0,0 +1,69 @@
+<!--
+
+ 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-content style="position: relative; width: 100%; height: 100%;"
+ ng-style="{'background-color': vm.layoutCtx.gridSettings.backgroundColor,
+ 'background-image': 'url('+vm.layoutCtx.gridSettings.backgroundImageUrl+')',
+ 'background-repeat': 'no-repeat',
+ 'background-attachment': 'scroll',
+ 'background-size': vm.layoutCtx.gridSettings.backgroundSizeMode || '100%',
+ 'background-position': '0% 0%'}">
+ <section ng-show="!loading && vm.noData()" layout-align="center center"
+ ng-style="{'color': vm.layoutCtx.gridSettings.color}"
+ style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;"
+ class="md-headline tb-absolute-fill">
+ <span translate ng-if="!vm.isEdit">
+ dashboard.no-widgets
+ </span>
+ <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget({event: $event})">
+ <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
+ {{ 'dashboard.add-widget' | translate }}
+ </md-button>
+ </section>
+ <tb-dashboard
+ dashboard-style="{'background-color': vm.layoutCtx.gridSettings.backgroundColor,
+ 'background-image': 'url('+vm.layoutCtx.gridSettings.backgroundImageUrl+')',
+ 'background-repeat': 'no-repeat',
+ 'background-attachment': 'scroll',
+ 'background-size': vm.layoutCtx.gridSettings.backgroundSizeMode || '100%',
+ 'background-position': '0% 0%'}"
+ widgets="vm.layoutCtx.widgets"
+ widget-layouts="vm.layoutCtx.widgetLayouts"
+ columns="vm.layoutCtx.gridSettings.columns"
+ margins="vm.layoutCtx.gridSettings.margins"
+ aliases-info="vm.dashboardCtx.aliasesInfo"
+ state-controller="vm.dashboardCtx.stateController"
+ dashboard-timewindow="vm.dashboardCtx.dashboardTimewindow"
+ is-edit="vm.isEdit"
+ is-mobile="vm.isMobile"
+ is-mobile-disabled="vm.widgetEditMode"
+ is-edit-action-enabled="vm.isEdit"
+ is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
+ is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
+ on-edit-widget="vm.editWidget(event, widget)"
+ on-export-widget="vm.exportWidget(event, widget)"
+ on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
+ on-widget-clicked="vm.widgetClicked(event, widget)"
+ prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
+ prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
+ on-remove-widget="vm.removeWidget(event, widget)"
+ get-st-diff="vm.getStDiff()"
+ on-init="vm.dashboardInited(dashboard)"
+ on-init-failed="vm.dashboardInitFailed(e)"
+ ignore-loading="vm.layoutCtx.ignoreLoading">
+ </tb-dashboard>
+</md-content>
ui/src/app/dashboard/layouts/index.js 25(+25 -0)
diff --git a/ui/src/app/dashboard/layouts/index.js b/ui/src/app/dashboard/layouts/index.js
new file mode 100644
index 0000000..6315565
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/index.js
@@ -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.
+ */
+
+import ManageDashboardLayoutsController from './manage-dashboard-layouts.controller';
+import SelectTargetLayoutController from './select-target-layout.controller';
+import DashboardLayoutDirective from './dashboard-layout.directive';
+
+export default angular.module('thingsboard.dashboard.layouts', [])
+ .controller('ManageDashboardLayoutsController', ManageDashboardLayoutsController)
+ .controller('SelectTargetLayoutController', SelectTargetLayoutController)
+ .directive('tbDashboardLayout', DashboardLayoutDirective)
+ .name;
diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
new file mode 100644
index 0000000..1ccf48f
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.controller.js
@@ -0,0 +1,83 @@
+/*
+ * 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 dashboardSettingsTemplate from '../dashboard-settings.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ManageDashboardLayoutsController($scope, $mdDialog, $document, dashboardUtils, layouts) {
+
+ var vm = this;
+
+ vm.openLayoutSettings = openLayoutSettings;
+ vm.cancel = cancel;
+ vm.save = save;
+
+ vm.layouts = layouts;
+ vm.displayLayouts = {
+ main: angular.isDefined(vm.layouts.main),
+ right: angular.isDefined(vm.layouts.right)
+ }
+
+ for (var l in vm.displayLayouts) {
+ if (!vm.layouts[l]) {
+ vm.layouts[l] = dashboardUtils.createDefaultLayoutData();
+ }
+ }
+
+ function openLayoutSettings($event, layoutId) {
+ var gridSettings = angular.copy(vm.layouts[layoutId].gridSettings);
+ $mdDialog.show({
+ controller: 'DashboardSettingsController',
+ controllerAs: 'vm',
+ templateUrl: dashboardSettingsTemplate,
+ locals: {
+ settings: null,
+ gridSettings: gridSettings
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (data) {
+ var gridSettings = data.gridSettings;
+ if (gridSettings) {
+ dashboardUtils.updateLayoutSettings(vm.layouts[layoutId], gridSettings);
+ }
+ $scope.theForm.$setDirty();
+ }, function () {
+ });
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ for (var l in vm.displayLayouts) {
+ if (!vm.displayLayouts[l]) {
+ if (vm.layouts[l]) {
+ delete vm.layouts[l];
+ }
+ }
+ }
+ $mdDialog.hide(vm.layouts);
+ }
+}
diff --git a/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html
new file mode 100644
index 0000000..d4d95db
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/manage-dashboard-layouts.tpl.html
@@ -0,0 +1,65 @@
+<!--
+
+ 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-dialog aria-label="{{ 'layout.manage' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ 'layout.manage' }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div layout="row" layout-align="start center">
+ <md-checkbox ng-disabled="true" flex aria-label="{{ 'layout.main' | translate }}"
+ ng-model="vm.displayLayouts.main">{{ 'layout.main' | translate }}
+ </md-checkbox>
+ <md-checkbox flex aria-label="{{ 'layout.right' | translate }}"
+ ng-model="vm.displayLayouts.right">{{ 'layout.right' | translate }}
+ </md-checkbox>
+ </div>
+ <div layout="row" layout-align="start center">
+ <md-button flex ng-show="vm.displayLayouts.main"
+ class="tb-layout-button md-raised md-primary" layout="column"
+ ng-click="vm.openLayoutSettings($event, 'main')">
+ <span translate>layout.main</span>
+ </md-button>
+ <md-button flex ng-show="vm.displayLayouts.right"
+ class="tb-layout-button md-raised md-primary" layout="column"
+ ng-click="vm.openLayoutSettings($event, 'right')">
+ <span translate>layout.right</span>
+ </md-button>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/layouts/select-target-layout.controller.js b/ui/src/app/dashboard/layouts/select-target-layout.controller.js
new file mode 100644
index 0000000..1af289a
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/select-target-layout.controller.js
@@ -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.
+ */
+
+/*@ngInject*/
+export default function SelectTargetLayoutController($scope, $mdDialog) {
+
+ var vm = this;
+
+ vm.cancel = cancel;
+ vm.selectLayout = selectLayout;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function selectLayout($event, layoutId) {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(layoutId);
+ }
+}
diff --git a/ui/src/app/dashboard/layouts/select-target-layout.tpl.html b/ui/src/app/dashboard/layouts/select-target-layout.tpl.html
new file mode 100644
index 0000000..f69f487
--- /dev/null
+++ b/ui/src/app/dashboard/layouts/select-target-layout.tpl.html
@@ -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.
+
+-->
+<md-dialog aria-label="{{ 'layout.select' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ 'layout.select' }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div layout="row" layout-align="start center">
+ <md-button flex class="tb-layout-button md-raised md-primary" layout="column"
+ ng-click="vm.selectLayout($event, 'main')">
+ <span translate>layout.main</span>
+ </md-button>
+ <md-button flex class="tb-layout-button md-raised md-primary" layout="column"
+ ng-click="vm.selectLayout($event, 'right')">
+ <span translate>layout.right</span>
+ </md-button>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
new file mode 100644
index 0000000..86eb9ec
--- /dev/null
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.controller.js
@@ -0,0 +1,82 @@
+/*
+ * 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 DashboardStateDialogController($scope, $mdDialog, $filter, dashboardUtils, isAdd, allStates, state) {
+
+ var vm = this;
+
+ vm.isAdd = isAdd;
+ vm.allStates = allStates;
+ vm.state = state;
+
+ vm.stateIdTouched = false;
+
+ if (vm.isAdd) {
+ vm.state = dashboardUtils.createDefaultState('', false);
+ vm.state.id = '';
+ vm.prevStateId = '';
+ } else {
+ vm.state = state;
+ vm.prevStateId = vm.state.id;
+ }
+
+ vm.cancel = cancel;
+ vm.save = save;
+
+ $scope.$watch("vm.state.name", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.state.name != null) {
+ checkStateName();
+ }
+ });
+
+ $scope.$watch("vm.state.id", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.state.id != null) {
+ checkStateId();
+ }
+ });
+
+ function checkStateName() {
+ if (!vm.stateIdTouched && vm.isAdd) {
+ vm.state.id = vm.state.name.toLowerCase().replace(/\W/g,"_");
+ }
+ var result = $filter('filter')(vm.allStates, {name: vm.state.name}, true);
+ if (result && result.length && result[0].id !== vm.prevStateId) {
+ $scope.theForm.name.$setValidity('stateExists', false);
+ } else {
+ $scope.theForm.name.$setValidity('stateExists', true);
+ }
+ }
+
+ function checkStateId() {
+ var result = $filter('filter')(vm.allStates, {id: vm.state.id}, true);
+ if (result && result.length && result[0].id !== vm.prevStateId) {
+ $scope.theForm.stateId.$setValidity('stateExists', false);
+ } else {
+ $scope.theForm.stateId.$setValidity('stateExists', true);
+ }
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ vm.state.id = vm.state.id.trim();
+ $mdDialog.hide(vm.state);
+ }
+}
diff --git a/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
new file mode 100644
index 0000000..fa45d7a
--- /dev/null
+++ b/ui/src/app/dashboard/states/dashboard-state-dialog.tpl.html
@@ -0,0 +1,72 @@
+<!--
+
+ 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-dialog class="dashboard-state" aria-label="{{'dashboard.state' | translate }}" style="min-width: 600px;">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2>{{ (vm.isAdd ? 'dashboard.add-state' : 'dashboard.edit-state') | translate }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'action.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading">
+ <md-input-container class="md-block">
+ <label translate>dashboard.state-name</label>
+ <input name="name" required ng-model="vm.state.name">
+ <div ng-messages="theForm.name.$error">
+ <div ng-message="required" translate>dashboard.state-name-required</div>
+ <div ng-message="stateExists" translate>dashboard.state-name-exists</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>dashboard.state-id</label>
+ <input name="stateId" ng-model="vm.state.id"
+ ng-change="vm.stateIdTouched = true"
+ ng-pattern="/^[a-zA-Z0-9_]*$/">
+ <div ng-messages="theForm.stateId.$error">
+ <div ng-message="required" translate>dashboard.state-id-required</div>
+ <div ng-message="stateExists" translate>dashboard.state-id-exists</div>
+ <div ng-message="pattern" translate>dashboard.invalid-state-id-format</div>
+ </div>
+ </md-input-container>
+ <md-checkbox flex aria-label="{{ 'dashboard.is-root-state' | translate }}"
+ ng-model="vm.state.root">{{ 'dashboard.is-root-state' | translate }}
+ </md-checkbox>
+ </fieldset>
+ </md-content>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ vm.isAdd ? 'Add' : 'Save' }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">
+ Cancel
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js
new file mode 100644
index 0000000..782f59e
--- /dev/null
+++ b/ui/src/app/dashboard/states/default-state-controller.js
@@ -0,0 +1,181 @@
+/*
+ * 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 DefaultStateController($scope, $location, $state, $stateParams, $translate, types, dashboardUtils) {
+
+ var vm = this;
+
+ vm.inited = false;
+
+ vm.openState = openState;
+ vm.updateState = updateState;
+ vm.navigatePrevState = navigatePrevState;
+ vm.getStateId = getStateId;
+ vm.getStateParams = getStateParams;
+
+ vm.getStateName = getStateName;
+
+ vm.displayStateSelection = displayStateSelection;
+
+ function openState(id, params) {
+ if (vm.states && vm.states[id]) {
+ if (!params) {
+ params = {};
+ }
+ var newState = {
+ id: id,
+ params: params
+ }
+ //append new state
+ vm.stateObject[0] = newState;
+ gotoState(vm.stateObject[0].id, true);
+ }
+ }
+
+ function updateState(id, params) {
+ if (vm.states && vm.states[id]) {
+ if (!params) {
+ params = {};
+ }
+ var newState = {
+ id: id,
+ params: params
+ }
+ //replace with new state
+ vm.stateObject[0] = newState;
+ gotoState(vm.stateObject[0].id, true);
+ }
+ }
+
+ function navigatePrevState(index) {
+ if (index < vm.stateObject.length-1) {
+ vm.stateObject.splice(index+1, vm.stateObject.length-index-1);
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+ }
+ }
+
+ function getStateId() {
+ return vm.stateObject[vm.stateObject.length-1].id;
+ }
+
+ function getStateParams() {
+ return vm.stateObject[vm.stateObject.length-1].params;
+ }
+
+ function getStateName(id, state) {
+ var result = '';
+ var translationId = types.translate.dashboardStatePrefix + id;
+ var translation = $translate.instant(translationId);
+ if (translation != translationId) {
+ result = translation;
+ } else {
+ result = state.name;
+ }
+ return result;
+ }
+
+ function parseState(stateJson) {
+ var result;
+ if (stateJson) {
+ try {
+ result = angular.fromJson(stateJson);
+ } catch (e) {
+ result = [ { id: null, params: {} } ];
+ }
+ }
+ if (!result) {
+ result = [];
+ }
+ if (!result.length) {
+ result[0] = { id: null, params: {} }
+ }
+ if (!result[0].id) {
+ result[0].id = dashboardUtils.getRootStateId(vm.states);
+ }
+ return result;
+ }
+
+ $scope.$watch('vm.states', function() {
+ if (vm.states) {
+ if (!vm.inited) {
+ vm.inited = true;
+ init();
+ }
+ }
+ });
+
+ function displayStateSelection() {
+ return vm.states && Object.keys(vm.states).length > 1;
+ }
+
+ function init() {
+ var initialState = $stateParams.state;
+ vm.stateObject = parseState(initialState);
+
+ gotoState(vm.stateObject[0].id, false);
+
+ $scope.$watchCollection(function(){
+ return $state.params;
+ }, function(){
+ var currentState = $state.params.state;
+ vm.stateObject = parseState(currentState);
+ });
+
+ $scope.$watch('vm.dashboardCtrl.dashboardCtx.state', function() {
+ if (vm.stateObject[0].id !== vm.dashboardCtrl.dashboardCtx.state) {
+ stopWatchStateObject();
+ vm.stateObject[0].id = vm.dashboardCtrl.dashboardCtx.state;
+ updateLocation();
+ watchStateObject();
+ }
+ });
+ watchStateObject();
+ }
+
+ function stopWatchStateObject() {
+ if (vm.stateObjectWatcher) {
+ vm.stateObjectWatcher();
+ vm.stateObjectWatcher = null;
+ }
+ }
+
+ function watchStateObject() {
+ vm.stateObjectWatcher = $scope.$watch('vm.stateObject', function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && newVal) {
+ gotoState(vm.stateObject[0].id, true);
+ }
+ }, true);
+ }
+
+ function gotoState(stateId, update) {
+ if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
+ vm.dashboardCtrl.openDashboardState(stateId);
+ if (update) {
+ updateLocation();
+ }
+ }
+ }
+
+ function updateLocation() {
+ if (vm.stateObject[0].id) {
+ $location.search({state : angular.toJson(vm.stateObject)});
+ }
+ }
+
+
+
+}
diff --git a/ui/src/app/dashboard/states/default-state-controller.tpl.html b/ui/src/app/dashboard/states/default-state-controller.tpl.html
new file mode 100644
index 0000000..9de1563
--- /dev/null
+++ b/ui/src/app/dashboard/states/default-state-controller.tpl.html
@@ -0,0 +1,22 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-select ng-show="vm.displayStateSelection()" aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.stateObject[0].id">
+ <md-option ng-repeat="(stateId, state) in vm.states" ng-value="stateId">
+ {{vm.getStateName(stateId, state)}}
+ </md-option>
+</md-select>
diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js
new file mode 100644
index 0000000..3eaf453
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.js
@@ -0,0 +1,243 @@
+/*
+ * 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-state-controller.scss';
+
+/*@ngInject*/
+export default function EntityStateController($scope, $location, $state, $stateParams, $q, $translate, types, dashboardUtils, entityService) {
+
+ var vm = this;
+
+ vm.inited = false;
+
+ vm.openState = openState;
+ vm.updateState = updateState;
+ vm.navigatePrevState = navigatePrevState;
+ vm.getStateId = getStateId;
+ vm.getStateParams = getStateParams;
+
+ vm.getStateName = getStateName;
+
+ vm.selectedStateIndex = -1;
+
+ function openState(id, params) {
+ if (vm.states && vm.states[id]) {
+ resolveEntity(params).then(
+ function success(entityName) {
+ params.entityName = entityName;
+ var newState = {
+ id: id,
+ params: params
+ }
+ //append new state
+ vm.stateObject.push(newState);
+ vm.selectedStateIndex = vm.stateObject.length-1;
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+ }
+ );
+ }
+ }
+
+ function updateState(id, params) {
+ if (vm.states && vm.states[id]) {
+ resolveEntity(params).then(
+ function success(entityName) {
+ params.entityName = entityName;
+ var newState = {
+ id: id,
+ params: params
+ }
+ //replace with new state
+ vm.stateObject[vm.stateObject.length - 1] = newState;
+ gotoState(vm.stateObject[vm.stateObject.length - 1].id, true);
+ }
+ );
+ }
+ }
+
+ function navigatePrevState(index) {
+ if (index < vm.stateObject.length-1) {
+ vm.stateObject.splice(index+1, vm.stateObject.length-index-1);
+ vm.selectedStateIndex = vm.stateObject.length-1;
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+ }
+ }
+
+ function getStateId() {
+ return vm.stateObject[vm.stateObject.length-1].id;
+ }
+
+ function getStateParams() {
+ return vm.stateObject[vm.stateObject.length-1].params;
+ }
+
+ function getStateName(index) {
+ var result = '';
+ if (vm.stateObject[index]) {
+ var params = vm.stateObject[index].params;
+ if (params && params.entityName) {
+ result = params.entityName;
+ } else {
+ var id = vm.stateObject[index].id;
+ var translationId = types.translate.dashboardStatePrefix + id;
+ var translation = $translate.instant(translationId);
+ if (translation != translationId) {
+ result = translation;
+ } else {
+ result = vm.states[vm.stateObject[index].id].name;
+ }
+ }
+ }
+ return result;
+ }
+
+ function resolveEntity(params) {
+ var deferred = $q.defer();
+ if (params && params.entityId && params.entityId.id && params.entityId.entityType) {
+ entityService.getEntity(params.entityId.entityType, params.entityId.id, {ignoreLoading: true, ignoreErrors: true}).then(
+ function success(entity) {
+ var entityName = entityService.entityName(params.entityId.entityType, entity);
+ deferred.resolve(entityName);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+
+ function parseState(stateJson) {
+ var result;
+ if (stateJson) {
+ try {
+ result = angular.fromJson(stateJson);
+ } catch (e) {
+ result = [ { id: null, params: {} } ];
+ }
+ }
+ if (!result) {
+ result = [];
+ }
+ if (!result.length) {
+ result[0] = { id: null, params: {} }
+ }
+ if (!result[0].id) {
+ result[0].id = dashboardUtils.getRootStateId(vm.states);
+ }
+ return result;
+ }
+
+ $scope.$watch('vm.states', function() {
+ if (vm.states) {
+ if (!vm.inited) {
+ vm.inited = true;
+ init();
+ }
+ }
+ });
+
+ function init() {
+ var initialState = $stateParams.state;
+ vm.stateObject = parseState(initialState);
+ vm.selectedStateIndex = vm.stateObject.length-1;
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, false);
+
+ $scope.$watchCollection(function() {
+ return $state.params;
+ }, function(){
+ var currentState = $state.params.state;
+ vm.stateObject = parseState(currentState);
+ });
+
+ $scope.$watch('vm.dashboardCtrl.dashboardCtx.state', function() {
+ if (vm.stateObject[vm.stateObject.length-1].id !== vm.dashboardCtrl.dashboardCtx.state) {
+ stopWatchStateObject();
+ vm.stateObject[vm.stateObject.length-1].id = vm.dashboardCtrl.dashboardCtx.state;
+ updateLocation();
+ watchStateObject();
+ }
+ });
+
+ watchStateObject();
+
+ if (vm.dashboardCtrl.isMobile) {
+ watchSelectedStateIndex();
+ }
+
+ $scope.$watch('vm.dashboardCtrl.isMobile', function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ if (vm.dashboardCtrl.isMobile) {
+ watchSelectedStateIndex();
+ } else {
+ stopWatchSelectedStateIndex();
+ }
+ }
+ });
+
+ }
+
+ function stopWatchStateObject() {
+ if (vm.stateObjectWatcher) {
+ vm.stateObjectWatcher();
+ vm.stateObjectWatcher = null;
+ }
+ }
+
+ function watchStateObject() {
+ vm.stateObjectWatcher = $scope.$watch('vm.stateObject', function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && newVal) {
+ vm.selectedStateIndex = vm.stateObject.length-1;
+ gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+ }
+ }, true);
+ }
+
+ function stopWatchSelectedStateIndex() {
+ if (vm.selectedStateIndexWatcher) {
+ vm.selectedStateIndexWatcher();
+ vm.selectedStateIndexWatcher = null;
+ }
+ }
+
+ function watchSelectedStateIndex() {
+ vm.selectedStateIndexWatcher = $scope.$watch('vm.selectedStateIndex', function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ navigatePrevState(vm.selectedStateIndex);
+ }
+ });
+ }
+
+ function gotoState(stateId, update) {
+ if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
+ vm.dashboardCtrl.openDashboardState(stateId);
+ if (update) {
+ updateLocation();
+ }
+ }
+ }
+
+ function updateLocation() {
+ if (vm.stateObject[vm.stateObject.length-1].id) {
+ $location.search({state : angular.toJson(vm.stateObject)});
+ }
+ }
+
+
+
+}
diff --git a/ui/src/app/dashboard/states/entity-state-controller.scss b/ui/src/app/dashboard/states/entity-state-controller.scss
new file mode 100644
index 0000000..2d79157
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.scss
@@ -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.
+ */
+
+.entity-state-controller {
+ .state-divider {
+ font-size: 28px;
+ padding-left: 15px;
+ padding-right: 15px;
+ }
+ .state-entry {
+ font-size: 22px;
+ outline: none;
+ }
+ md-select {
+ .md-text {
+ font-size: 22px;
+ font-weight: bold;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/states/entity-state-controller.tpl.html b/ui/src/app/dashboard/states/entity-state-controller.tpl.html
new file mode 100644
index 0000000..4139be6
--- /dev/null
+++ b/ui/src/app/dashboard/states/entity-state-controller.tpl.html
@@ -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.
+
+-->
+<div class="entity-state-controller">
+ <div ng-if="!vm.dashboardCtrl.isMobile || vm.stateObject.length===1" layout="row" layout-align="start center">
+ <div layout="row" layout-align="start center" ng-repeat="state in vm.stateObject track by $index">
+ <span class='state-divider' ng-if="$index"> > </span>
+ <span class='state-entry' ng-style="{fontWeight: $last ? 'bold' : 'normal',
+ cursor: $last ? 'default' : 'pointer'}" ng-click="vm.navigatePrevState($index)">
+ {{vm.getStateName($index)}}
+ </span>
+ </div>
+ </div>
+ <md-select ng-if="vm.dashboardCtrl.isMobile && vm.stateObject.length > 1" aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.selectedStateIndex">
+ <md-option ng-repeat="state in vm.stateObject track by $index" ng-value="$index">
+ {{vm.getStateName($index)}}
+ </md-option>
+ </md-select>
+</div>
\ No newline at end of file
ui/src/app/dashboard/states/index.js 29(+29 -0)
diff --git a/ui/src/app/dashboard/states/index.js b/ui/src/app/dashboard/states/index.js
new file mode 100644
index 0000000..ce59e38
--- /dev/null
+++ b/ui/src/app/dashboard/states/index.js
@@ -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.
+ */
+
+import ManageDashboardStatesController from './manage-dashboard-states.controller';
+import DashboardStateDialogController from './dashboard-state-dialog.controller';
+import SelectTargetStateController from './select-target-state.controller';
+import StatesComponentDirective from './states-component.directive';
+import StatesControllerService from './states-controller.service';
+
+export default angular.module('thingsboard.dashboard.states', [])
+ .controller('ManageDashboardStatesController', ManageDashboardStatesController)
+ .controller('DashboardStateDialogController', DashboardStateDialogController)
+ .controller('SelectTargetStateController', SelectTargetStateController)
+ .directive('tbStatesComponent', StatesComponentDirective)
+ .factory('statesControllerService', StatesControllerService)
+ .name;
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
new file mode 100644
index 0000000..452e94f
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js
@@ -0,0 +1,198 @@
+/*
+ * 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 './manage-dashboard-states.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardStateDialogTemplate from './dashboard-state-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ManageDashboardStatesController($scope, $mdDialog, $filter, $document, $translate, states) {
+
+ var vm = this;
+
+ vm.allStates = [];
+ for (var id in states) {
+ var state = states[id];
+ state.id = id;
+ vm.allStates.push(state);
+ }
+
+ vm.states = [];
+ vm.statesCount = 0;
+
+ vm.query = {
+ order: 'name',
+ limit: 5,
+ page: 1,
+ search: null
+ };
+
+ vm.enterFilterMode = enterFilterMode;
+ vm.exitFilterMode = exitFilterMode;
+ vm.onReorder = onReorder;
+ vm.onPaginate = onPaginate;
+ vm.addState = addState;
+ vm.editState = editState;
+ vm.deleteState = deleteState;
+
+ vm.cancel = cancel;
+ vm.save = save;
+
+ $scope.$watch("vm.query.search", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+ updateStates();
+ }
+ });
+
+ updateStates ();
+
+ function updateStates () {
+ var result = $filter('orderBy')(vm.allStates, vm.query.order);
+ if (vm.query.search != null) {
+ result = $filter('filter')(result, {$: vm.query.search});
+ }
+ vm.statesCount = result.length;
+ var startIndex = vm.query.limit * (vm.query.page - 1);
+ vm.states = result.slice(startIndex, startIndex + vm.query.limit);
+ }
+
+ function enterFilterMode () {
+ vm.query.search = '';
+ }
+
+ function exitFilterMode () {
+ vm.query.search = null;
+ updateStates();
+ }
+
+ function onReorder () {
+ updateStates();
+ }
+
+ function onPaginate () {
+ updateStates();
+ }
+
+ function addState ($event) {
+ openStateDialog($event, null, true);
+ }
+
+ function editState ($event, alertRule) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ openStateDialog($event, alertRule, false);
+ }
+
+ function openStateDialog($event, state, isAdd) {
+ var prevStateId = null;
+ if (!isAdd) {
+ prevStateId = state.id;
+ }
+ $mdDialog.show({
+ controller: 'DashboardStateDialogController',
+ controllerAs: 'vm',
+ templateUrl: dashboardStateDialogTemplate,
+ parent: angular.element($document[0].body),
+ locals: {isAdd: isAdd, allStates: vm.allStates, state: angular.copy(state)},
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (state) {
+ saveState(state, prevStateId);
+ updateStates();
+ });
+ }
+
+ function getStateIndex(id) {
+ var result = $filter('filter')(vm.allStates, {id: id});
+ if (result && result.length) {
+ return vm.allStates.indexOf(result[0]);
+ }
+ return -1;
+ }
+
+ function saveState(state, prevStateId) {
+ if (prevStateId) {
+ var index = getStateIndex(prevStateId);
+ if (index > -1) {
+ vm.allStates[index] = state;
+ }
+ } else {
+ vm.allStates.push(state);
+ }
+ if (state.root) {
+ for (var i=0; i < vm.allStates.length; i++) {
+ var otherState = vm.allStates[i];
+ if (otherState.id !== state.id) {
+ otherState.root = false;
+ }
+ }
+ }
+ $scope.theForm.$setDirty();
+ }
+
+ function deleteState ($event, state) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (state) {
+ var title = $translate.instant('dashboard.delete-state-title');
+ var content = $translate.instant('dashboard.delete-state-text', {stateName: state.name});
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title(title)
+ .htmlContent(content)
+ .ariaLabel(title)
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+
+ confirm._options.skipHide = true;
+ confirm._options.fullscreen = true;
+
+ $mdDialog.show(confirm).then(function () {
+ var index = getStateIndex(state.id);
+ if (index > -1) {
+ vm.allStates.splice(index, 1);
+ }
+ $scope.theForm.$setDirty();
+ updateStates();
+ });
+
+
+ }
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ var savedStates = {};
+ for (var i=0;i<vm.allStates.length;i++) {
+ var state = vm.allStates[i];
+ var id = state.id;
+ delete state.id;
+ savedStates[id] = state;
+ }
+ $mdDialog.hide(savedStates);
+ }
+}
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.scss b/ui/src/app/dashboard/states/manage-dashboard-states.scss
new file mode 100644
index 0000000..53e6724
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.scss
@@ -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.
+ */
+
+.manage-dashboard-states {
+ table.md-table {
+ tbody {
+ tr {
+ td {
+ &.tb-action-cell {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ min-width: 100px;
+ max-width: 100px;
+ width: 100px;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
new file mode 100644
index 0000000..151c05a
--- /dev/null
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
@@ -0,0 +1,127 @@
+<!--
+
+ 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-dialog aria-label="{{ 'dashboard.manage-states' | translate }}" style="min-width: 600px;">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ 'dashboard.manage-states' }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div class="manage-dashboard-states" layout="column">
+ <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search === null">
+ <div class="md-toolbar-tools">
+ <span translate>dashboard.states</span>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.addState($event)">
+ <md-icon>add</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.add-state' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+ <md-icon>search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.search' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search != null">
+ <div class="md-toolbar-tools">
+ <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.search-states' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-input-container md-theme="tb-search-input" flex>
+ <label> </label>
+ <input ng-model="vm.query.search" placeholder="{{ 'dashboard.search-states' | translate }}"/>
+ </md-input-container>
+ <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
+ <md-icon aria-label="Close" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-table-container>
+ <table md-table>
+ <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+ <tr md-row>
+ <th md-column md-order-by="name"><span translate>dashboard.state-name</span></th>
+ <th md-column md-order-by="id"><span translate>dashboard.state-id</span></th>
+ <th md-column md-order-by="root"><span translate>dashboard.is-root-state</span></th>
+ <th md-column><span> </span></th>
+ </tr>
+ </thead>
+ <tbody md-body>
+ <tr md-row md-select="state" ng-disabled="state.root" md-select-id="id" md-auto-select ng-repeat="state in vm.states">
+ <td md-cell>{{state.name}}</td>
+ <td md-cell>{{state.id}}</td>
+ <td md-cell>
+ <md-checkbox aria-label="{{'dashboard.is-root-state' | translate }}"
+ disabled ng-model="state.root">
+ </md-checkbox>
+ </td>
+ <td md-cell class="tb-action-cell">
+ <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"
+ ng-click="vm.editState($event, state)">
+ <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.edit-state' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button ng-show="!state.root" class="md-icon-button" aria-label="Delete" ng-click="vm.deleteState($event, state)">
+ <md-icon aria-label="Delete" class="material-icons">delete</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.delete-state' | translate }}
+ </md-tooltip>
+ </md-button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </md-table-container>
+ <md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
+ md-page="vm.query.page" md-total="{{vm.statesCount}}"
+ md-on-paginate="vm.onPaginate" md-page-select>
+ </md-table-pagination>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/select-target-state.controller.js b/ui/src/app/dashboard/states/select-target-state.controller.js
new file mode 100644
index 0000000..fa62eef
--- /dev/null
+++ b/ui/src/app/dashboard/states/select-target-state.controller.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.
+ */
+
+/*@ngInject*/
+export default function SelectTargetStateController($scope, $mdDialog, dashboardUtils, states) {
+
+ var vm = this;
+ vm.states = states;
+ vm.stateId = dashboardUtils.getRootStateId(vm.states);
+
+ vm.cancel = cancel;
+ vm.save = save;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.stateId);
+ }
+}
diff --git a/ui/src/app/dashboard/states/select-target-state.tpl.html b/ui/src/app/dashboard/states/select-target-state.tpl.html
new file mode 100644
index 0000000..c874a07
--- /dev/null
+++ b/ui/src/app/dashboard/states/select-target-state.tpl.html
@@ -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.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.select-state' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ 'dashboard.select-state' }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <md-select required aria-label="{{ 'dashboard.state' | translate }}" ng-model="vm.stateId">
+ <md-option ng-repeat="(stateId, state) in vm.states" ng-value="stateId">
+ {{state.name}}
+ </md-option>
+ </md-select>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/states/states-component.directive.js b/ui/src/app/dashboard/states/states-component.directive.js
new file mode 100644
index 0000000..fb5e77c
--- /dev/null
+++ b/ui/src/app/dashboard/states/states-component.directive.js
@@ -0,0 +1,117 @@
+/*
+ * 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 StatesComponent($compile, $templateCache, $controller, statesControllerService) {
+
+ var linker = function (scope, element) {
+
+ function destroyStateController() {
+ if (scope.statesController && angular.isFunction(scope.statesController.$onDestroy)) {
+ scope.statesController.$onDestroy();
+ }
+ }
+
+ function init() {
+
+ var stateController = scope.dashboardCtrl.dashboardCtx.stateController;
+
+ stateController.openState = function(id, params) {
+ if (scope.statesController) {
+ scope.statesController.openState(id, params);
+ }
+ }
+
+ stateController.updateState = function(id, params) {
+ if (scope.statesController) {
+ scope.statesController.updateState(id, params);
+ }
+ }
+
+ stateController.navigatePrevState = function(index) {
+ if (scope.statesController) {
+ scope.statesController.navigatePrevState(index);
+ }
+ }
+
+ stateController.getStateId = function() {
+ if (scope.statesController) {
+ return scope.statesController.getStateId();
+ } else {
+ return '';
+ }
+ }
+
+ stateController.getStateParams = function() {
+ if (scope.statesController) {
+ return scope.statesController.getStateParams();
+ } else {
+ return {};
+ }
+ }
+ }
+
+ scope.$on('$destroy', function callOnDestroyHook() {
+ destroyStateController();
+ });
+
+ scope.$watch('scope.dashboardCtrl', function() {
+ if (scope.dashboardCtrl.dashboardCtx) {
+ init();
+ }
+ })
+
+ scope.$watch('statesControllerId', function(newValue) {
+ if (newValue) {
+ if (scope.statesController) {
+ destroyStateController();
+ }
+ var statesControllerInfo = statesControllerService.getStateController(scope.statesControllerId);
+ if (!statesControllerInfo) {
+ //fallback to default
+ statesControllerInfo = statesControllerService.getStateController('default');
+ }
+ var template = $templateCache.get(statesControllerInfo.templateUrl);
+ element.html(template);
+ var locals = {};
+ angular.extend(locals, {$scope: scope, $element: element});
+ var controller = $controller(statesControllerInfo.controller, locals, true, 'vm');
+ controller.instance = controller();
+ scope.statesController = controller.instance;
+ scope.statesController.dashboardCtrl = scope.dashboardCtrl;
+ scope.statesController.states = scope.states;
+ $compile(element.contents())(scope);
+ }
+ });
+
+ scope.$watch('states', function() {
+ if (scope.statesController) {
+ scope.statesController.states = scope.states;
+ }
+ });
+
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ statesControllerId: '=',
+ dashboardCtrl: '=',
+ states: '='
+ }
+ };
+}
diff --git a/ui/src/app/dashboard/states/states-controller.service.js b/ui/src/app/dashboard/states/states-controller.service.js
new file mode 100644
index 0000000..e4c1f29
--- /dev/null
+++ b/ui/src/app/dashboard/states/states-controller.service.js
@@ -0,0 +1,60 @@
+/*
+ * 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 defaultStateControllerTemplate from './default-state-controller.tpl.html';
+import entityStateControllerTemplate from './entity-state-controller.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import DefaultStateController from './default-state-controller';
+import EntityStateController from './entity-state-controller';
+
+/*@ngInject*/
+export default function StatesControllerService() {
+
+ var statesControllers = {};
+ statesControllers['default'] = {
+ controller: DefaultStateController,
+ templateUrl: defaultStateControllerTemplate
+ };
+ statesControllers['entity'] = {
+ controller: EntityStateController,
+ templateUrl: entityStateControllerTemplate
+ };
+
+ var service = {
+ registerStatesController: registerStatesController,
+ getStateControllers: getStateControllers,
+ getStateController: getStateController
+ };
+
+ return service;
+
+ function registerStatesController(id, stateControllerInfo) {
+ statesControllers[id] = stateControllerInfo;
+ }
+
+ function getStateControllers() {
+ return statesControllers;
+ }
+
+ function getStateController(id) {
+ return statesControllers[id];
+ }
+
+}
ui/src/app/device/devices.tpl.html 12(+7 -5)
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 90909e3..2f90db8 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -34,15 +34,17 @@
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
- device-id="vm.grid.operatingItem().id.id"
- device-name="vm.grid.operatingItem().name"
- default-attribute-scope="{{vm.types.deviceAttributesScope.client.value}}">
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.device}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.attributesScope.client.value}}">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
- device-id="vm.grid.operatingItem().id.id"
- device-name="vm.grid.operatingItem().name"
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.device}}"
+ entity-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.latestTelemetry.value}}"
disable-attribute-scope-selection="true">
</tb-attribute-table>
ui/src/app/device/index.js 6(+0 -6)
diff --git a/ui/src/app/device/index.js b/ui/src/app/device/index.js
index b84497a..ea42ee8 100644
--- a/ui/src/app/device/index.js
+++ b/ui/src/app/device/index.js
@@ -25,10 +25,7 @@ import {DeviceController, DeviceCardController} from './device.controller';
import AssignDeviceToCustomerController from './assign-to-customer.controller';
import AddDevicesToCustomerController from './add-devices-to-customer.controller';
import ManageDeviceCredentialsController from './device-credentials.controller';
-import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
-import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
import DeviceDirective from './device.directive';
-import AttributeTableDirective from './attribute/attribute-table.directive';
export default angular.module('thingsboard.device', [
uiRouter,
@@ -44,8 +41,5 @@ export default angular.module('thingsboard.device', [
.controller('AssignDeviceToCustomerController', AssignDeviceToCustomerController)
.controller('AddDevicesToCustomerController', AddDevicesToCustomerController)
.controller('ManageDeviceCredentialsController', ManageDeviceCredentialsController)
- .controller('AddAttributeDialogController', AddAttributeDialogController)
- .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
.directive('tbDevice', DeviceDirective)
- .directive('tbAttributeTable', AttributeTableDirective)
.name;
ui/src/app/entity/aliases-entity-select.directive.js 154(+154 -0)
diff --git a/ui/src/app/entity/aliases-entity-select.directive.js b/ui/src/app/entity/aliases-entity-select.directive.js
new file mode 100644
index 0000000..7512409
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select.directive.js
@@ -0,0 +1,154 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './aliases-entity-select.scss';
+
+import $ from 'jquery';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import aliasesEntitySelectButtonTemplate from './aliases-entity-select-button.tpl.html';
+import aliasesEntitySelectPanelTemplate from './aliases-entity-select-panel.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+/*@ngInject*/
+export default function AliasesEntitySelectDirective($compile, $templateCache, $mdMedia, types, $mdPanel, $document, $translate) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ /* tbAliasesEntitySelect (ng-model)
+ * {
+ * "aliasId": {
+ * alias: alias,
+ * entityType: entityType,
+ * entityId: entityId
+ * }
+ * }
+ */
+
+ var template = $templateCache.get(aliasesEntitySelectButtonTemplate);
+
+ scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top';
+
+ element.html(template);
+
+ scope.openEditMode = function (event) {
+ if (scope.disabled) {
+ return;
+ }
+ var position;
+ var panelHeight = $mdMedia('min-height: 350px') ? 250 : 150;
+ var panelWidth = 300;
+ var offset = element[0].getBoundingClientRect();
+ var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
+ var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
+ var yPosition;
+ var xPosition;
+ if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
+ yPosition = $mdPanel.yPosition.ABOVE;
+ } else {
+ yPosition = $mdPanel.yPosition.BELOW;
+ }
+ if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
+ xPosition = $mdPanel.xPosition.CENTER;
+ } else {
+ xPosition = $mdPanel.xPosition.ALIGN_START;
+ }
+ position = $mdPanel.newPanelPosition()
+ .relativeTo(element)
+ .addPanelPosition(xPosition, yPosition);
+ var config = {
+ attachTo: angular.element($document[0].body),
+ controller: 'AliasesEntitySelectPanelController',
+ controllerAs: 'vm',
+ templateUrl: aliasesEntitySelectPanelTemplate,
+ panelClass: 'tb-aliases-entity-select-panel',
+ position: position,
+ fullscreen: false,
+ locals: {
+ 'entityAliases': angular.copy(scope.model),
+ 'entityAliasesInfo': scope.entityAliasesInfo,
+ 'onEntityAliasesUpdate': function (entityAliases) {
+ scope.model = entityAliases;
+ scope.updateView();
+ }
+ },
+ openFrom: event,
+ clickOutsideToClose: true,
+ escapeToClose: true,
+ focusOnOpen: false
+ };
+ $mdPanel.open(config);
+ }
+
+ scope.updateView = function () {
+ var value = angular.copy(scope.model);
+ ngModelCtrl.$setViewValue(value);
+ updateDisplayValue();
+ }
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ scope.model = angular.copy(value);
+ updateDisplayValue();
+ }
+ }
+
+ function updateDisplayValue() {
+ var displayValue;
+ var singleValue = true;
+ var currentAliasId;
+ for (var aliasId in scope.model) {
+ if (!currentAliasId) {
+ currentAliasId = aliasId;
+ } else {
+ singleValue = false;
+ break;
+ }
+ }
+ if (singleValue && currentAliasId) {
+ var entityId = scope.model[currentAliasId].entityId;
+ var entitiesInfo = scope.entityAliasesInfo[currentAliasId];
+ for (var i=0;i<entitiesInfo.length;i++) {
+ if (entitiesInfo[i].id === entityId) {
+ displayValue = entitiesInfo[i].name;
+ break;
+ }
+ }
+ } else {
+ displayValue = $translate.instant('entity.entities');
+ }
+ scope.displayValue = displayValue;
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ entityAliasesInfo:'='
+ },
+ link: linker
+ };
+
+}
+
+/* eslint-enable angular/angularelement */
\ No newline at end of file
ui/src/app/entity/aliases-entity-select.scss 46(+46 -0)
diff --git a/ui/src/app/entity/aliases-entity-select.scss b/ui/src/app/entity/aliases-entity-select.scss
new file mode 100644
index 0000000..55b0442
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select.scss
@@ -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.
+ */
+
+.md-panel {
+ &.tb-aliases-entity-select-panel {
+ position: absolute;
+ }
+}
+
+.tb-aliases-entity-select-panel {
+ max-height: 150px;
+ @media (min-height: 350px) {
+ max-height: 250px;
+ }
+ min-width: 300px;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
+ 0 13px 19px 2px rgba(0, 0, 0, 0.14),
+ 0 5px 24px 4px rgba(0, 0, 0, 0.12);
+ overflow-x: hidden;
+ overflow-y: auto;
+ md-content {
+ background-color: #fff;
+ }
+}
+
+.tb-aliases-entity-select {
+ span {
+ pointer-events: all;
+ cursor: pointer;
+ }
+}
diff --git a/ui/src/app/entity/aliases-entity-select-button.tpl.html b/ui/src/app/entity/aliases-entity-select-button.tpl.html
new file mode 100644
index 0000000..b0b69f5
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select-button.tpl.html
@@ -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.
+
+-->
+
+<section class="tb-aliases-entity-select" layout='row' layout-align="start center" ng-style="{minHeight: '32px', padding: '0 6px'}">
+ <md-button class="md-icon-button" aria-label="{{ 'entity.select-entities' | translate }}" ng-click="openEditMode($event)">
+ <md-tooltip md-direction="{{tooltipDirection}}">
+ {{ 'entity.select-entities' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'entity.select-entities' | translate }}" class="material-icons">devices_other</md-icon>
+ </md-button>
+ <span hide-xs hide-sm ng-click="openEditMode($event)">
+ <md-tooltip md-direction="{{tooltipDirection}}">
+ {{ 'entity.select-entities' | translate }}
+ </md-tooltip>
+ {{displayValue}}
+ </span>
+</section>
diff --git a/ui/src/app/entity/aliases-entity-select-panel.controller.js b/ui/src/app/entity/aliases-entity-select-panel.controller.js
new file mode 100644
index 0000000..4128f94
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select-panel.controller.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*@ngInject*/
+export default function AliasesEntitySelectPanelController(mdPanelRef, $scope, types, entityAliases, entityAliasesInfo, onEntityAliasesUpdate) {
+
+ var vm = this;
+ vm._mdPanelRef = mdPanelRef;
+ vm.entityAliases = entityAliases;
+ vm.entityAliasesInfo = entityAliasesInfo;
+ vm.onEntityAliasesUpdate = onEntityAliasesUpdate;
+
+ $scope.$watch('vm.entityAliases', function () {
+ if (onEntityAliasesUpdate) {
+ onEntityAliasesUpdate(vm.entityAliases);
+ }
+ }, true);
+}
diff --git a/ui/src/app/entity/aliases-entity-select-panel.tpl.html b/ui/src/app/entity/aliases-entity-select-panel.tpl.html
new file mode 100644
index 0000000..d9c5f2f
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select-panel.tpl.html
@@ -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.
+
+-->
+<md-content flex layout="column">
+ <section flex layout="column">
+ <md-content flex class="md-padding" layout="column">
+ <div flex layout="row" ng-repeat="(aliasId, entityAlias) in vm.entityAliases">
+ <md-input-container flex>
+ <label>{{entityAlias.alias}}</label>
+ <md-select ng-model="vm.entityAliases[aliasId].entityId">
+ <md-option ng-repeat="entityInfo in vm.entityAliasesInfo[aliasId]" ng-value="entityInfo.id">
+ {{entityInfo.name}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ </div>
+ </md-content>
+ </section>
+</md-content>
diff --git a/ui/src/app/entity/attribute/add-attribute-dialog.controller.js b/ui/src/app/entity/attribute/add-attribute-dialog.controller.js
new file mode 100644
index 0000000..af0c32e
--- /dev/null
+++ b/ui/src/app/entity/attribute/add-attribute-dialog.controller.js
@@ -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.
+ */
+/*@ngInject*/
+export default function AddAttributeDialogController($scope, $mdDialog, types, attributeService, entityType, entityId, attributeScope) {
+
+ var vm = this;
+
+ vm.attribute = {};
+
+ vm.valueTypes = types.valueType;
+
+ vm.valueType = types.valueType.string;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function add() {
+ $scope.theForm.$setPristine();
+ attributeService.saveEntityAttributes(entityType, entityId, attributeScope, [vm.attribute]).then(
+ function success() {
+ $mdDialog.hide();
+ }
+ );
+ }
+
+ $scope.$watch('vm.valueType', function() {
+ if (vm.valueType === types.valueType.boolean) {
+ vm.attribute.value = false;
+ } else {
+ vm.attribute.value = null;
+ }
+ });
+}
diff --git a/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html b/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html
new file mode 100644
index 0000000..18f99fc
--- /dev/null
+++ b/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html
@@ -0,0 +1,95 @@
+<!--
+
+ 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-dialog aria-label="{{ 'attribute.add' | translate }}" style="min-width: 400px;">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>attribute.add</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading">
+ <md-input-container class="md-block">
+ <label translate>attribute.key</label>
+ <input required name="key" ng-model="vm.attribute.key">
+ <div ng-messages="theForm.key.$error">
+ <div translate ng-message="required">attribute.key-required</div>
+ </div>
+ </md-input-container>
+ <section layout="row">
+ <md-input-container flex="40" class="md-block" style="width: 200px;">
+ <label translate>value.type</label>
+ <md-select ng-model="vm.valueType" ng-disabled="loading()">
+ <md-option ng-repeat="type in vm.valueTypes" ng-value="type">
+ <md-icon md-svg-icon="{{ type.icon }}"></md-icon>
+ <span>{{type.name | translate}}</span>
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container ng-if="vm.valueType===vm.valueTypes.string" flex="60" class="md-block">
+ <label translate>value.string-value</label>
+ <input required name="value" ng-model="vm.attribute.value">
+ <div ng-messages="theForm.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="vm.valueType===vm.valueTypes.integer" flex="60" class="md-block">
+ <label translate>value.integer-value</label>
+ <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attribute.value">
+ <div ng-messages="theForm.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ <div translate ng-message="pattern">value.invalid-integer-value</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="vm.valueType===vm.valueTypes.double" flex="60" class="md-block">
+ <label translate>value.double-value</label>
+ <input required name="value" type="number" step="any" ng-model="vm.attribute.value">
+ <div ng-messages="theForm.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ </div>
+ </md-input-container>
+ <div layout="column" layout-align="center" flex="60" ng-if="vm.valueType===vm.valueTypes.boolean">
+ <md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0px;">
+ {{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }}
+ </md-checkbox>
+ </div>
+ </section>
+ </fieldset>
+ </md-content>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
new file mode 100644
index 0000000..e5822a4
--- /dev/null
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -0,0 +1,159 @@
+/*
+ * 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 selectTargetStateTemplate from '../../dashboard/states/select-target-state.tpl.html';
+import selectTargetLayoutTemplate from '../../dashboard/layouts/select-target-layout.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, $q, $document, itembuffer, dashboardService, entityId, entityType, entityName, widget) {
+
+ var vm = this;
+
+ vm.widget = widget;
+ vm.dashboardId = null;
+ vm.addToDashboardType = 0;
+ vm.newDashboard = {};
+ vm.openDashboard = false;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function selectTargetState($event, dashboard) {
+ var deferred = $q.defer();
+ var states = dashboard.configuration.states;
+ var stateIds = Object.keys(states);
+ if (stateIds.length > 1) {
+ $mdDialog.show({
+ controller: 'SelectTargetStateController',
+ controllerAs: 'vm',
+ templateUrl: selectTargetStateTemplate,
+ parent: angular.element($document[0].body),
+ locals: {
+ states: states
+ },
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: $event
+ }).then(
+ function success(stateId) {
+ deferred.resolve(stateId);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+
+ } else {
+ deferred.resolve(stateIds[0]);
+ }
+ return deferred.promise;
+ }
+
+ function selectTargetLayout($event, dashboard, targetState) {
+ var deferred = $q.defer();
+ var layouts = dashboard.configuration.states[targetState].layouts;
+ var layoutIds = Object.keys(layouts);
+ if (layoutIds.length > 1) {
+ $mdDialog.show({
+ controller: 'SelectTargetLayoutController',
+ controllerAs: 'vm',
+ templateUrl: selectTargetLayoutTemplate,
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: $event
+ }).then(
+ function success(layoutId) {
+ deferred.resolve(layoutId);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.resolve(layoutIds[0]);
+ }
+ return deferred.promise;
+ }
+
+ function add($event) {
+ if (vm.addToDashboardType === 0) {
+ dashboardService.getDashboard(vm.dashboardId).then(
+ function success(dashboard) {
+ selectTargetState($event, dashboard).then(
+ function(targetState) {
+ selectTargetLayout($event, dashboard, targetState).then(
+ function(targetLayout) {
+ addWidgetToDashboard(dashboard, targetState, targetLayout);
+ }
+ );
+ }
+ );
+ },
+ function fail() {}
+ );
+ } else {
+ addWidgetToDashboard(vm.newDashboard, 'default', 'main');
+ }
+
+ }
+
+ function addWidgetToDashboard(theDashboard, targetState, targetLayout) {
+ var aliasesInfo = {
+ datasourceAliases: {},
+ targetDeviceAliases: {}
+ };
+ aliasesInfo.datasourceAliases[0] = {
+ aliasName: entityName,
+ entityType: entityType,
+ entityFilter: {
+ useFilter: false,
+ entityNameFilter: '',
+ entityList: [entityId]
+ }
+ };
+ itembuffer.addWidgetToDashboard(theDashboard, targetState, targetLayout, vm.widget, aliasesInfo, null, 48, null, -1, -1).then(
+ function(theDashboard) {
+ dashboardService.saveDashboard(theDashboard).then(
+ function success(dashboard) {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide();
+ if (vm.openDashboard) {
+ var stateParams = {
+ dashboardId: dashboard.id.id
+ }
+ var stateIds = Object.keys(dashboard.configuration.states);
+ var stateIndex = stateIds.indexOf(targetState);
+ if (stateIndex > 0) {
+ stateParams.state = angular.toJson([ {id: targetState, params: {}} ]);
+ }
+ $state.go('home.dashboards.dashboard', stateParams);
+ }
+ }
+ );
+ }
+ );
+ }
+
+}
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
new file mode 100644
index 0000000..d7fe890
--- /dev/null
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
@@ -0,0 +1,81 @@
+<!--
+
+ 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-dialog aria-label="{{ 'attribute.add-widget-to-dashboard' | translate }}" style="min-width: 400px;">
+ <form name="theForm" ng-submit="vm.add($event)">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>attribute.add-widget-to-dashboard</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading">
+ <md-radio-group ng-model="vm.addToDashboardType" class="md-primary">
+ <md-radio-button flex ng-value=0 class="md-primary md-align-top-left md-radio-interactive">
+ <section flex layout="column" style="width: 300px;">
+ <span translate style="padding-bottom: 10px;">dashboard.select-existing</span>
+ <tb-dashboard-autocomplete the-form="theForm"
+ ng-disabled="loading || vm.addToDashboardType != 0"
+ tb-required="vm.addToDashboardType === 0"
+ ng-model="vm.dashboardId"
+ select-first-dashboard="false">
+ </tb-dashboard-autocomplete>
+ </section>
+ </md-radio-button>
+ <md-radio-button flex ng-value=1 class="md-primary md-align-top-left md-radio-interactive">
+ <section flex layout="column" style="width: 300px;">
+ <span translate>dashboard.create-new</span>
+ <md-input-container class="md-block">
+ <label translate>dashboard.new-dashboard-title</label>
+ <input ng-required="vm.addToDashboardType === 1" name="title" ng-model="vm.newDashboard.title">
+ <div ng-messages="theForm.title.$error">
+ <div translate ng-message="required">dashboard.title-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ </md-radio-button>
+ </md-radio-group>
+ </fieldset>
+ </md-content>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-checkbox
+ ng-model="vm.openDashboard"
+ aria-label="{{ 'dashboard.open-dashboard' | translate }}"
+ style="margin-bottom: 0px; padding-right: 20px;">
+ {{ 'dashboard.open-dashboard' | translate }}
+ </md-checkbox>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js
new file mode 100644
index 0000000..e05fcb6
--- /dev/null
+++ b/ui/src/app/entity/attribute/attribute-table.directive.js
@@ -0,0 +1,433 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import 'angular-material-data-table/dist/md-data-table.min.css';
+import './attribute-table.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import attributeTableTemplate from './attribute-table.tpl.html';
+import addAttributeDialogTemplate from './add-attribute-dialog.tpl.html';
+import addWidgetToDashboardDialogTemplate from './add-widget-to-dashboard-dialog.tpl.html';
+import editAttributeValueTemplate from './edit-attribute-value.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import EditAttributeValueController from './edit-attribute-value.controller';
+
+/*@ngInject*/
+export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog,
+ $document, $translate, $filter, utils, types, dashboardService, attributeService, widgetService) {
+
+ var linker = function (scope, element, attrs) {
+
+ var template = $templateCache.get(attributeTableTemplate);
+
+ element.html(template);
+
+ var getAttributeScopeByValue = function(attributeScopeValue) {
+ if (scope.types.latestTelemetry.value === attributeScopeValue) {
+ return scope.types.latestTelemetry;
+ }
+ for (var attrScope in scope.attributeScopes) {
+ if (scope.attributeScopes[attrScope].value === attributeScopeValue) {
+ return scope.attributeScopes[attrScope];
+ }
+ }
+ }
+
+ scope.types = types;
+
+ scope.entityType = attrs.entityType;
+ scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
+
+ if (scope.entityType === types.entityType.device) {
+ scope.attributeScopes = types.attributesScope;
+ scope.attributeScopeSelectionReadonly = false;
+ } else {
+ scope.attributeScopes = {};
+ scope.attributeScopes.server = types.attributesScope.server;
+ scope.attributeScopeSelectionReadonly = true;
+ if (scope.attributeScope != types.latestTelemetry) {
+ scope.attributeScope = scope.attributeScopes.server.value;
+ }
+ }
+
+ scope.attributes = {
+ count: 0,
+ data: []
+ };
+
+ scope.selectedAttributes = [];
+ scope.mode = 'default'; // 'widget'
+ scope.subscriptionId = null;
+
+ scope.query = {
+ order: 'key',
+ limit: 5,
+ page: 1,
+ search: null
+ };
+
+ scope.$watch("entityId", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ scope.resetFilter();
+ scope.getEntityAttributes(false, true);
+ }
+ });
+
+ scope.$watch("attributeScope", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ scope.mode = 'default';
+ scope.query.search = null;
+ scope.selectedAttributes = [];
+ scope.getEntityAttributes(false, true);
+ }
+ });
+
+ scope.resetFilter = function() {
+ scope.mode = 'default';
+ scope.query.search = null;
+ scope.selectedAttributes = [];
+ scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
+ }
+
+ scope.enterFilterMode = function() {
+ scope.query.search = '';
+ }
+
+ scope.exitFilterMode = function() {
+ scope.query.search = null;
+ scope.getEntityAttributes();
+ }
+
+ scope.$watch("query.search", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && scope.query.search != null) {
+ scope.getEntityAttributes();
+ }
+ });
+
+ function success(attributes, update, apply) {
+ scope.attributes = attributes;
+ if (!update) {
+ scope.selectedAttributes = [];
+ }
+ if (apply) {
+ scope.$digest();
+ }
+ }
+
+ scope.onReorder = function() {
+ scope.getEntityAttributes(false, false);
+ }
+
+ scope.onPaginate = function() {
+ scope.getEntityAttributes(false, false);
+ }
+
+ scope.getEntityAttributes = function(forceUpdate, reset) {
+ if (scope.attributesDeferred) {
+ scope.attributesDeferred.resolve();
+ }
+ if (scope.entityId && scope.entityType && scope.attributeScope) {
+ if (reset) {
+ scope.attributes = {
+ count: 0,
+ data: []
+ };
+ }
+ scope.checkSubscription();
+ scope.attributesDeferred = attributeService.getEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value,
+ scope.query, function(attributes, update, apply) {
+ success(attributes, update || forceUpdate, apply);
+ }
+ );
+ } else {
+ var deferred = $q.defer();
+ scope.attributesDeferred = deferred;
+ success({
+ count: 0,
+ data: []
+ });
+ deferred.resolve();
+ }
+ }
+
+ scope.checkSubscription = function() {
+ var newSubscriptionId = null;
+ if (scope.entityId && scope.entityType && scope.attributeScope.clientSide && scope.mode != 'widget') {
+ newSubscriptionId = attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value);
+ }
+ if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) {
+ attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
+ }
+ scope.subscriptionId = newSubscriptionId;
+ }
+
+ scope.$on('$destroy', function() {
+ if (scope.subscriptionId) {
+ attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
+ }
+ });
+
+ scope.editAttribute = function($event, attribute) {
+ if (!scope.attributeScope.clientSide) {
+ $event.stopPropagation();
+ $mdEditDialog.show({
+ controller: EditAttributeValueController,
+ templateUrl: editAttributeValueTemplate,
+ locals: {attributeValue: attribute.value,
+ save: function (model) {
+ var updatedAttribute = angular.copy(attribute);
+ updatedAttribute.value = model.value;
+ attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then(
+ function success() {
+ scope.getEntityAttributes();
+ }
+ );
+ }},
+ targetEvent: $event
+ });
+ }
+ }
+
+ scope.addAttribute = function($event) {
+ if (!scope.attributeScope.clientSide) {
+ $event.stopPropagation();
+ $mdDialog.show({
+ controller: 'AddAttributeDialogController',
+ controllerAs: 'vm',
+ templateUrl: addAttributeDialogTemplate,
+ parent: angular.element($document[0].body),
+ locals: {entityType: scope.entityType, entityId: scope.entityId, attributeScope: scope.attributeScope.value},
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ scope.getEntityAttributes();
+ });
+ }
+ }
+
+ scope.deleteAttributes = function($event) {
+ if (!scope.attributeScope.clientSide) {
+ $event.stopPropagation();
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title($translate.instant('attribute.delete-attributes-title', {count: scope.selectedAttributes.length}, 'messageformat'))
+ .htmlContent($translate.instant('attribute.delete-attributes-text'))
+ .ariaLabel($translate.instant('attribute.delete-attributes'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then(
+ function success() {
+ scope.selectedAttributes = [];
+ scope.getEntityAttributes();
+ }
+ )
+ });
+ }
+ }
+
+ scope.nextWidget = function() {
+ if (scope.widgetsCarousel.index < scope.widgetsList.length-1) {
+ scope.widgetsCarousel.index++;
+ }
+ }
+
+ scope.prevWidget = function() {
+ if (scope.widgetsCarousel.index > 0) {
+ scope.widgetsCarousel.index--;
+ }
+ }
+
+ scope.enterWidgetMode = function() {
+
+ if (scope.widgetsIndexWatch) {
+ scope.widgetsIndexWatch();
+ scope.widgetsIndexWatch = null;
+ }
+
+ if (scope.widgetsBundleWatch) {
+ scope.widgetsBundleWatch();
+ scope.widgetsBundleWatch = null;
+ }
+
+ scope.mode = 'widget';
+ scope.checkSubscription();
+ scope.widgetsList = [];
+ scope.widgetsListCache = [];
+ scope.widgetsLoaded = false;
+ scope.widgetsCarousel = {
+ index: 0
+ }
+ scope.widgetsBundle = null;
+ scope.firstBundle = true;
+ scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
+
+ scope.aliasesInfo = {
+ entityAliases: {
+ '1': {alias: scope.entityName, entityType: scope.entityType, entityId: scope.entityId}
+ },
+ entityAliasesInfo: {
+ '1': [
+ {name: scope.entityName, entityType: scope.entityType, id: scope.entityId}
+ ]
+ }
+ };
+
+ var dataKeyType = scope.attributeScope === types.latestTelemetry ?
+ types.dataKeyType.timeseries : types.dataKeyType.attribute;
+
+ var datasource = {
+ type: types.datasourceType.entity,
+ entityAliasId: '1',
+ dataKeys: []
+ }
+ var i = 0;
+ for (var attr =0; attr < scope.selectedAttributes.length;attr++) {
+ var attribute = scope.selectedAttributes[attr];
+ var dataKey = {
+ name: attribute.key,
+ label: attribute.key,
+ type: dataKeyType,
+ color: utils.getMaterialColor(i),
+ settings: {},
+ _hash: Math.random()
+ }
+ datasource.dataKeys.push(dataKey);
+ i++;
+ }
+
+ scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function(newVal, prevVal) {
+ if (scope.mode === 'widget' && (newVal != prevVal)) {
+ var index = scope.widgetsCarousel.index;
+ for (var i = 0; i < scope.widgetsList.length; i++) {
+ scope.widgetsList[i].splice(0, scope.widgetsList[i].length);
+ if (i === index) {
+ scope.widgetsList[i].push(scope.widgetsListCache[i][0]);
+ }
+ }
+ }
+ });
+
+ scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function(newVal, prevVal) {
+ if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) {
+ scope.widgetsList = [];
+ scope.widgetsListCache = [];
+ scope.widgetsCarousel.index = 0;
+ scope.firstBundle = false;
+ if (scope.widgetsBundle) {
+ scope.widgetsLoaded = false;
+ var bundleAlias = scope.widgetsBundle.alias;
+ var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid;
+ widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
+ function success(widgetTypes) {
+
+ widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
+
+ for (var i = 0; i < widgetTypes.length; i++) {
+ var widgetType = widgetTypes[i];
+ var widgetInfo = widgetService.toWidgetInfo(widgetType);
+ if (widgetInfo.type !== types.widgetType.static.value) {
+ var sizeX = widgetInfo.sizeX * 2;
+ var sizeY = widgetInfo.sizeY * 2;
+ var col = Math.floor(Math.max(0, (20 - sizeX) / 2));
+ var widget = {
+ isSystemType: isSystem,
+ bundleAlias: bundleAlias,
+ typeAlias: widgetInfo.alias,
+ type: widgetInfo.type,
+ title: widgetInfo.widgetName,
+ sizeX: sizeX,
+ sizeY: sizeY,
+ row: 0,
+ col: col,
+ config: angular.fromJson(widgetInfo.defaultConfig)
+ };
+
+ widget.config.title = widgetInfo.widgetName;
+ widget.config.datasources = [datasource];
+ var length;
+ if (scope.attributeScope === types.latestTelemetry && widgetInfo.type !== types.widgetType.rpc.value) {
+ length = scope.widgetsListCache.push([widget]);
+ scope.widgetsList.push(length === 1 ? [widget] : []);
+ } else if (widgetInfo.type === types.widgetType.latest.value) {
+ length = scope.widgetsListCache.push([widget]);
+ scope.widgetsList.push(length === 1 ? [widget] : []);
+ }
+ }
+ }
+ scope.widgetsLoaded = true;
+ }
+ );
+ }
+ }
+ });
+ }
+
+ scope.exitWidgetMode = function() {
+ if (scope.widgetsBundleWatch) {
+ scope.widgetsBundleWatch();
+ scope.widgetsBundleWatch = null;
+ }
+ if (scope.widgetsIndexWatch) {
+ scope.widgetsIndexWatch();
+ scope.widgetsIndexWatch = null;
+ }
+ scope.selectedWidgetsBundleAlias = null;
+ scope.mode = 'default';
+ scope.getEntityAttributes(true);
+ }
+
+ scope.getServerTimeDiff = function() {
+ return dashboardService.getServerTimeDiff();
+ }
+
+ scope.addWidgetToDashboard = function($event) {
+ if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
+ var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
+ $event.stopPropagation();
+ $mdDialog.show({
+ controller: 'AddWidgetToDashboardDialogController',
+ controllerAs: 'vm',
+ templateUrl: addWidgetToDashboardDialogTemplate,
+ parent: angular.element($document[0].body),
+ locals: {entityId: scope.entityId, entityType: scope.entityType, entityName: scope.entityName, widget: angular.copy(widget)},
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+
+ });
+ }
+ }
+
+ scope.loading = function() {
+ return $rootScope.loading;
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ entityId: '=',
+ entityName: '=',
+ disableAttributeScopeSelection: '@?'
+ }
+ };
+}
diff --git a/ui/src/app/entity/attribute/attribute-table.scss b/ui/src/app/entity/attribute/attribute-table.scss
new file mode 100644
index 0000000..3358230
--- /dev/null
+++ b/ui/src/app/entity/attribute/attribute-table.scss
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../../scss/constants';
+
+$md-light: rgba(255, 255, 255, 100%);
+$md-edit-icon-fill: #757575;
+
+md-toolbar.md-table-toolbar.alternate {
+ .md-toolbar-tools {
+ md-icon {
+ color: $md-light;
+ }
+ }
+}
+
+.md-table {
+ .md-cell {
+ ng-md-icon {
+ fill: $md-edit-icon-fill;
+ float: right;
+ height: 16px;
+ }
+ }
+}
+
+.widgets-carousel {
+ position: relative;
+ margin: 0px;
+
+ min-height: 150px !important;
+
+ tb-dashboard {
+ #gridster-parent {
+ padding: 0 7px;
+ }
+ }
+}
\ No newline at end of file
ui/src/app/entity/attribute/attribute-table.tpl.html 210(+210 -0)
diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html
new file mode 100644
index 0000000..bf3b8cd
--- /dev/null
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -0,0 +1,210 @@
+<!--
+
+ 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-content flex class="md-padding tb-absolute-fill" layout="column">
+ <section layout="row" ng-show="!disableAttributeScopeSelection">
+ <md-input-container class="md-block" style="width: 200px;">
+ <label translate>attribute.attributes-scope</label>
+ <md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
+ <md-option ng-repeat="scope in attributeScopes" ng-value="scope">
+ {{scope.name | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ </section>
+ <div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
+ <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
+ && !selectedAttributes.length
+ && query.search === null">
+ <div class="md-toolbar-tools">
+ <span translate>{{ attributeScope.name }}</span>
+ <span flex></span>
+ <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="addAttribute($event)">
+ <md-icon>add</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.add' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" ng-click="enterFilterMode()">
+ <md-icon>search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="getEntityAttributes()">
+ <md-icon>refresh</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.refresh' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
+ && !selectedAttributes.length
+ && query.search != null">
+ <div class="md-toolbar-tools">
+ <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-input-container md-theme="tb-search-input" flex>
+ <label> </label>
+ <input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="exitFilterMode()">
+ <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar alternate" ng-show="mode==='default' && selectedAttributes.length">
+ <div class="md-toolbar-tools">
+ <span translate="{{attributeScope === types.latestTelemetry
+ ? 'attribute.selected-telemetry'
+ : 'attribute.selected-attributes'}}"
+ translate-values="{count: selectedAttributes.length}"
+ translate-interpolation="messageformat"></span>
+ <span flex></span>
+ <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="deleteAttributes($event)">
+ <md-icon>delete</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.delete' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-accent md-hue-2 md-raised" ng-click="enterWidgetMode()">
+ <md-tooltip md-direction="top">
+ {{ 'attribute.show-on-widget' | translate }}
+ </md-tooltip>
+ <md-icon>now_widgets</md-icon>
+ <span translate>attribute.show-on-widget</span>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar alternate" ng-show="mode==='widget'">
+ <div class="md-toolbar-tools">
+ <div flex layout="row" layout-align="start">
+ <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
+ <tb-widgets-bundle-select flex-offset="5"
+ flex
+ ng-model="widgetsBundle"
+ select-first-bundle="false"
+ select-bundle-alias="selectedWidgetsBundleAlias">
+ </tb-widgets-bundle-select>
+ </div>
+ <md-button ng-show="widgetsList.length > 0" class="md-accent md-hue-2 md-raised" ng-click="addWidgetToDashboard($event)">
+ <md-tooltip md-direction="top">
+ {{ 'attribute.add-to-dashboard' | translate }}
+ </md-tooltip>
+ <md-icon>dashboard</md-icon>
+ <span translate>attribute.add-to-dashboard</span>
+ </md-button>
+ <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="exitWidgetMode()">
+ <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-table-container ng-show="mode!='widget'">
+ <table md-table md-row-select multiple="" ng-model="selectedAttributes" md-progress="attributesDeferred.promise">
+ <thead md-head md-order="query.order" md-on-reorder="onReorder">
+ <tr md-row>
+ <th md-column md-order-by="lastUpdateTs"><span>Last update time</span></th>
+ <th md-column md-order-by="key"><span>Key</span></th>
+ <th md-column>Value</th>
+ </tr>
+ </thead>
+ <tbody md-body>
+ <tr md-row md-select="attribute" md-select-id="key" md-auto-select ng-repeat="attribute in attributes.data">
+ <td md-cell>{{attribute.lastUpdateTs | date : 'yyyy-MM-dd HH:mm:ss'}}</td>
+ <td md-cell>{{attribute.key}}</td>
+ <td md-cell ng-click="editAttribute($event, attribute)">
+ <span>{{attribute.value}}</span>
+ <span ng-show="!attributeScope.clientSide"><ng-md-icon size="16" icon="edit"></ng-md-icon></span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </md-table-container>
+ <md-table-pagination ng-show="mode!='widget'" md-limit="query.limit" md-limit-options="[5, 10, 15]"
+ md-page="query.page" md-total="{{attributes.count}}"
+ md-on-paginate="onPaginate" md-page-select>
+ </md-table-pagination>
+ <ul flex rn-carousel ng-if="mode==='widget'" class="widgets-carousel"
+ rn-carousel-index="widgetsCarousel.index"
+ rn-carousel-buffered
+ rn-carousel-transition="fadeAndSlide"
+ rn-swipe-disabled="true">
+ <li ng-repeat="widgets in widgetsList">
+ <tb-dashboard
+ aliases-info="aliasesInfo"
+ widgets="widgets"
+ get-st-diff="getServerTimeDiff()"
+ columns="20"
+ is-edit="false"
+ is-mobile-disabled="true"
+ is-edit-action-enabled="false"
+ is-remove-action-enabled="false">
+ </tb-dashboard>
+ </li>
+ <span translate ng-if="widgetsLoaded &&
+ widgetsList.length === 0 &&
+ widgetsBundle"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
+ <span translate ng-if="!widgetsBundle"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="md-headline tb-absolute-fill">widget.select-widgets-bundle</span>
+ <div ng-show="widgetsList.length > 1"
+ style="position: absolute; left: 0; height: 100%;" layout="column" layout-align="center">
+ <md-button ng-show="widgetsCarousel.index > 0"
+ class="md-icon-button"
+ ng-click="prevWidget()">
+ <md-icon>navigate_before</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'attribute.prev-widget' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ <div ng-show="widgetsList.length > 1"
+ style="position: absolute; right: 0; height: 100%;" layout="column" layout-align="center">
+ <md-button ng-show="widgetsCarousel.index < widgetsList.length-1"
+ class="md-icon-button"
+ ng-click="nextWidget()">
+ <md-icon>navigate_next</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'attribute.next-widget' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ <div style="position: absolute; bottom: 0; width: 100%; font-size: 24px;" layout="row" layout-align="center">
+ <div rn-carousel-indicators
+ ng-if="widgetsList.length > 1"
+ slides="widgetsList"
+ rn-carousel-index="widgetsCarousel.index">
+ </div>
+ </div>
+ </ul>
+ </div>
+</md-content>
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 0000000..fba5b4d
--- /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 0000000..8ce8d81
--- /dev/null
+++ b/ui/src/app/entity/attribute/edit-attribute-value.tpl.html
@@ -0,0 +1,72 @@
+<!--
+
+ 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-edit-dialog>
+ <form name="editDialog" ng-submit="submit()">
+ <div layout="column" class="md-content" style="width: 400px;">
+ <fieldset>
+ <section layout="row">
+ <md-input-container flex="40" class="md-block">
+ <label translate>value.type</label>
+ <md-select ng-model="valueType">
+ <md-option ng-repeat="type in valueTypes" ng-value="type">
+ <md-icon md-svg-icon="{{ type.icon }}"></md-icon>
+ <span>{{type.name | translate}}</span>
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container ng-if="valueType===valueTypes.string" flex="60" class="md-block">
+ <label translate>value.string-value</label>
+ <input required name="value" ng-model="model.value">
+ <div ng-messages="editDialog.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="valueType===valueTypes.integer" flex="60" class="md-block">
+ <label translate>value.integer-value</label>
+ <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="model.value">
+ <div ng-messages="editDialog.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ <div translate ng-message="pattern">value.invalid-integer-value</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="valueType===valueTypes.double" flex="60" class="md-block">
+ <label translate>value.double-value</label>
+ <input required name="value" type="number" step="any" ng-model="model.value">
+ <div ng-messages="editDialog.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ </div>
+ </md-input-container>
+ <div layout="column" layout-align="center" flex="60" ng-if="valueType===valueTypes.boolean">
+ <md-checkbox ng-model="model.value" style="margin-bottom: 0px;">
+ {{ (model.value ? 'value.true' : 'value.false') | translate }}
+ </md-checkbox>
+ </div>
+ </section>
+ </fieldset>
+ </div>
+ <div layout="row" layout-align="end" class="md-actions">
+ <md-button ng-click="dismiss()">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ <md-button ng-disabled="editDialog.$invalid || !editDialog.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.update' | translate }}
+ </md-button>
+ </div>
+ </form>
+</md-edit-dialog>
\ No newline at end of file
ui/src/app/entity/entity-aliases.controller.js 221(+221 -0)
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 0000000..4eb1737
--- /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<datasources.length;i++) {
+ var datasource = datasources[i];
+ if (datasource.type === types.datasourceType.entity && datasource.entityAliasId) {
+ widgetsTitleList = vm.aliasToWidgetsMap[datasource.entityAliasId];
+ if (!widgetsTitleList) {
+ widgetsTitleList = [];
+ vm.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList;
+ }
+ widgetsTitleList.push(widget.config.title);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (vm.isSingleEntityAlias) {
+ checkEntityAlias(vm.singleEntityAlias);
+ }
+
+ for (aliasId in config.entityAliases) {
+ var entityAlias = config.entityAliases[aliasId];
+ var result = {id: aliasId, alias: entityAlias.alias, entityType: entityAlias.entityType, entityFilter: entityAlias.entityFilter, changed: true};
+ checkEntityAlias(result);
+ vm.entityAliases.push(result);
+ }
+ }
+
+ function checkEntityAlias(entityAlias) {
+ if (!entityAlias.entityType) {
+ entityAlias.entityType = types.entityType.device;
+ }
+ if (!entityAlias.entityFilter || entityAlias.entityFilter == null) {
+ entityAlias.entityFilter = {
+ useFilter: false,
+ entityNameFilter: '',
+ entityList: [],
+ };
+ }
+ }
+
+ function onFilterEntityChanged(entity, entityAlias) {
+ if (entityAlias) {
+ if (!entityAlias.alias || entityAlias.alias.length == 0) {
+ entityAlias.changed = false;
+ }
+ if (!entityAlias.changed && entity && entityAlias.entityType) {
+ entityAlias.alias = entityService.entityName(entityAlias.entityType, entity);
+ }
+ }
+ }
+
+ function addAlias() {
+ var aliasId = 0;
+ for (var a in vm.entityAliases) {
+ aliasId = Math.max(vm.entityAliases[a].id, aliasId);
+ }
+ aliasId++;
+ var entityAlias = {id: aliasId, alias: '', entityType: types.entityType.device,
+ entityFilter: {useFilter: false, entityNameFilter: '', entityList: []}, changed: false};
+ vm.entityAliases.push(entityAlias);
+ }
+
+ function removeAlias($event, entityAlias) {
+ var index = vm.entityAliases.indexOf(entityAlias);
+ if (index > -1) {
+ var widgetsTitleList = vm.aliasToWidgetsMap[entityAlias.id];
+ if (widgetsTitleList) {
+ var widgetsListHtml = '';
+ for (var t in widgetsTitleList) {
+ widgetsListHtml += '<br/>\'' + 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}));
+ }
+ }
+
+}
ui/src/app/entity/entity-aliases.scss 28(+28 -0)
diff --git a/ui/src/app/entity/entity-aliases.scss b/ui/src/app/entity/entity-aliases.scss
new file mode 100644
index 0000000..b108dc0
--- /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;
+ }
+ }
+}
ui/src/app/entity/entity-aliases.tpl.html 106(+106 -0)
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 0000000..bc9e269
--- /dev/null
+++ b/ui/src/app/entity/entity-aliases.tpl.html
@@ -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.
+
+-->
+<md-dialog class="tb-aliases-dialog" style="width: 700px;" aria-label="{{ vm.title | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2>{{ vm.isSingleEntityAlias ? ('entity.configure-alias' | translate:vm.singleEntityAlias ) : (vm.title | translate) }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div ng-show="vm.isSingleEntityAlias" layout="row">
+ <tb-entity-type-select style="min-width: 100px;"
+ ng-model="vm.singleEntityAlias.entityType"
+ allowed-entity-types="vm.allowedEntityTypes">
+ </tb-entity-type-select>
+ <tb-entity-filter flex entity-type="vm.singleEntityAlias.entityType" ng-model="vm.singleEntityAlias.entityFilter">
+ </tb-entity-filter>
+ </div>
+ <div ng-show="!vm.isSingleEntityAlias" flex layout="row" layout-align="start center">
+ <span flex="5"></span>
+ <div flex layout="row" layout-align="start center"
+ style="padding: 0 0 0 10px; margin: 5px;">
+ <span translate flex="20" style="min-width: 100px;">entity.alias</span>
+ <span translate flex="20" style="min-width: 100px;">entity.type</span>
+ <span translate flex="60" style="min-width: 190px; padding-left: 10px;">entity.entities</span>
+ <span style="min-width: 40px;"></span>
+ </div>
+ </div>
+ <div ng-show="!vm.isSingleEntityAlias" style="max-height: 500px; overflow: auto; padding-bottom: 20px;">
+ <div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="entityAlias in vm.entityAliases track by $index">
+ <span flex="5">{{$index + 1}}.</span>
+ <div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
+ <md-input-container flex="20" style="min-width: 100px;" md-no-float class="md-block">
+ <input required ng-change="entityAlias.changed=true" name="alias" placeholder="{{ 'entity.alias' | translate }}" ng-model="entityAlias.alias">
+ <div ng-messages="aliasForm.alias.$error">
+ <div translate ng-message="required">entity.alias-required</div>
+ </div>
+ </md-input-container>
+ <section flex="20" layout="column" style="min-width: 100px;" >
+ <tb-entity-type-select hide-label style="padding-left: 10px;"
+ ng-model="entityAlias.entityType"
+ allowed-entity-types="vm.allowedEntityTypes">
+ </tb-entity-type-select>
+ </section>
+ <section flex="60" layout="column">
+ <tb-entity-filter style="padding-left: 10px;"
+ entity-type="entityAlias.entityType"
+ ng-model="entityAlias.entityFilter"
+ on-matching-entity-change="vm.onFilterEntityChanged(entity, entityAlias)">
+ </tb-entity-filter>
+ </section>
+ <md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
+ ng-click="vm.removeAlias($event, entityAlias)" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'entity.remove-alias' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ </div>
+ </div>
+ <div ng-show="!vm.isSingleEntityAlias && !vm.disableAdd" style="padding-bottom: 10px;">
+ <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'entity.add-alias' | translate }}
+ </md-tooltip>
+ <span translate>action.add</span>
+ </md-button>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
ui/src/app/entity/entity-filter.directive.js 234(+234 -0)
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 0000000..777dbb8
--- /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<scope.model.entityList.length;i++) {
+ value.entityList.push(scope.model.entityList[i].id.id);
+ }
+ }
+ updateMatchingEntity();
+ ngModelCtrl.$setViewValue(value);
+ scope.updateValidity();
+ }
+ }, true);
+ scope.useFilterDeregistration = scope.$watch('model.useFilter', function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ value.useFilter = scope.model.useFilter;
+ updateMatchingEntity();
+ ngModelCtrl.$setViewValue(value);
+ scope.updateValidity();
+ }
+ });
+ scope.entityNameFilterDeregistration = scope.$watch('model.entityNameFilter', function (newNameFilter, prevNameFilter) {
+ if (ngModelCtrl.$viewValue) {
+ if (!angular.equals(newNameFilter, prevNameFilter)) {
+ var value = ngModelCtrl.$viewValue;
+ value.entityNameFilter = scope.model.entityNameFilter;
+ processEntityNameFilter(value.entityNameFilter).then(
+ function(entity) {
+ scope.model.matchingFilterEntity = entity;
+ updateMatchingEntity();
+ ngModelCtrl.$setViewValue(value);
+ scope.updateValidity();
+ }
+ );
+ }
+ }
+ });
+
+ scope.matchingEntityDeregistration = scope.$watch('model.matchingEntity', function (newMatchingEntity, prevMatchingEntity) {
+ if (!angular.equals(newMatchingEntity, prevMatchingEntity)) {
+ if (scope.onMatchingEntityChange) {
+ scope.onMatchingEntityChange({entity: newMatchingEntity});
+ }
+ }
+ });
+ }
+
+ $compile(element.contents())(scope);
+
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ link: linker,
+ scope: {
+ entityType: '=',
+ isEdit: '=',
+ onMatchingEntityChange: '&'
+ }
+ };
+
+}
ui/src/app/entity/entity-filter.scss 45(+45 -0)
diff --git a/ui/src/app/entity/entity-filter.scss b/ui/src/app/entity/entity-filter.scss
new file mode 100644
index 0000000..1525842
--- /dev/null
+++ b/ui/src/app/entity/entity-filter.scss
@@ -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.
+ */
+.tb-entity-filter {
+ #entity_list_chips {
+ .md-chips {
+ padding-bottom: 1px;
+ }
+ }
+ .entity-name-filter-input {
+ margin-top: 10px;
+ margin-bottom: 0px;
+ .md-errors-spacer {
+ min-height: 0px;
+ }
+ }
+ .tb-filter-switch {
+ padding-left: 10px;
+ .filter-switch {
+ margin: 0;
+ }
+ .filter-label {
+ margin: 5px 0;
+ }
+ }
+ .tb-error-messages {
+ margin-top: -11px;
+ height: 35px;
+ .tb-error-message {
+ padding-left: 1px;
+ }
+ }
+}
\ No newline at end of file
ui/src/app/entity/entity-filter.tpl.html 67(+67 -0)
diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html
new file mode 100644
index 0000000..508e4ba
--- /dev/null
+++ b/ui/src/app/entity/entity-filter.tpl.html
@@ -0,0 +1,67 @@
+<!--
+
+ 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.
+
+-->
+<section layout='column' class="tb-entity-filter">
+ <section layout='row'>
+ <section layout="column" flex ng-show="!model.useFilter">
+ <md-chips flex
+ id="entity_list_chips"
+ ng-required="!useFilter"
+ ng-model="model.entityList" md-autocomplete-snap
+ md-require-match="true">
+ <md-autocomplete
+ md-no-cache="true"
+ id="entity"
+ md-selected-item="selectedEntity"
+ md-search-text="entitySearchText"
+ md-items="item in fetchEntities(entitySearchText, 10)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{ 'entity.entity-list' | translate }}">
+ <md-item-template>
+ <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{itemName(item)}}</span>
+ </md-item-template>
+ <md-not-found>
+ <span translate translate-values='{ entity: entitySearchText }'>entity.no-entities-matching</span>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <span>
+ <strong>{{itemName($chip)}}</strong>
+ </span>
+ </md-chip-template>
+ </md-chips>
+ </section>
+ <section layout="row" flex ng-show="model.useFilter">
+ <md-input-container flex class="entity-name-filter-input">
+ <label translate>entity.name-starts-with</label>
+ <input ng-model="model.entityNameFilter" aria-label="{{ 'entity.name-starts-with' | translate }}">
+ </md-input-container>
+ </section>
+ <section class="tb-filter-switch" layout="column" layout-align="center center">
+ <label class="tb-small filter-label" translate>entity.use-entity-name-filter</label>
+ <md-switch class="filter-switch" ng-model="model.useFilter" aria-label="use-filter-switcher">
+ </md-switch>
+ </section>
+ </section>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+ <div translate ng-message="entityList" class="tb-error-message">entity.entity-list-empty</div>
+ <div translate ng-message="entityNameFilter" class="tb-error-message">entity.entity-name-filter-required</div>
+ <div translate translate-values='{ entity: model.entityNameFilter }' ng-message="entityNameFilterEntityMatch"
+ class="tb-error-message">entity.entity-name-filter-no-entity-matched</div>
+ </div>
+</section>
\ No newline at end of file
ui/src/app/entity/entity-type-select.directive.js 120(+120 -0)
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 0000000..6a5a045
--- /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: "=?"
+ }
+ };
+}
ui/src/app/entity/entity-type-select.scss 18(+18 -0)
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 0000000..58ff6cc
--- /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 0000000..e31cbf5
--- /dev/null
+++ b/ui/src/app/entity/entity-type-select.tpl.html
@@ -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.
+
+-->
+<md-input-container>
+ <label ng-if="showLabel">{{ 'entity.type' | translate }}</label>
+ <md-select ng-model="entityType" class="tb-entity-type-select" aria-label="{{ 'entity.type' | translate }}">
+ <md-option ng-repeat="type in entityTypes" ng-value="type">
+ {{typeName(type) | translate}}
+ </md-option>
+ </md-select>
+</md-input-container>
\ No newline at end of file
ui/src/app/entity/index.js 35(+35 -0)
diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js
new file mode 100644
index 0000000..07c0862
--- /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/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js
index 86cd676..57bbcd3 100644
--- a/ui/src/app/global-interceptor.service.js
+++ b/ui/src/app/global-interceptor.service.js
@@ -174,7 +174,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
}
}
- if (unhandled) {
+ if (unhandled && !ignoreErrors) {
if (rejection.data && !rejection.data.message) {
getToast().showError(rejection.data);
} else if (rejection.data && rejection.data.message) {
diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js
index 62d6c2d..22f9d75 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",
ui/src/app/import-export/import-export.service.js 200(+131 -69)
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 0c04291..af04687 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 = {
@@ -332,45 +332,77 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
// Widget functions
- function exportWidget(dashboard, widget) {
- var widgetItem = itembuffer.prepareWidgetItem(dashboard, widget);
+ function exportWidget(dashboard, sourceState, sourceLayout, widget) {
+ var widgetItem = itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
var name = widgetItem.widget.config.title;
name = name.toLowerCase().replace(/\W/g,"_");
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]);
+ }
+ }
+ }
+ return aliasesInfo;
+ }
+
+ function prepareEntityAlias(aliasInfo) {
+ var entityFilter;
+ var entityType;
if (aliasInfo.deviceId) {
- deviceFilter = {
+ entityFilter = {
useFilter: false,
- deviceNameFilter: '',
- deviceList: [aliasInfo.deviceId]
+ entityNameFilter: '',
+ entityList: [aliasInfo.deviceId]
}
- delete aliasInfo.deviceId;
+ entityType = types.entityType.device;
+ } else if (aliasInfo.deviceFilter) {
+ entityFilter = {
+ useFilter: aliasInfo.deviceFilter.useFilter,
+ entityNameFilter: aliasInfo.deviceFilter.deviceNameFilter,
+ entityList: aliasInfo.deviceFilter.deviceList
+ }
+ entityType = types.entityType.device;
} else {
- deviceFilter = aliasInfo.deviceFilter;
+ entityFilter = aliasInfo.entityFilter;
+ entityType = aliasInfo.entityType;
}
return {
- alias: aliasInfo.aliasName,
- deviceFilter: deviceFilter
+ aliasName: aliasInfo.aliasName,
+ entityType: entityType,
+ entityFilter: entityFilter
};
}
- function importWidget($event, dashboard, onAliasesUpdate) {
+ function importWidget($event, dashboard, targetState, targetLayoutFunction, onAliasesUpdateFunction) {
+ var deferred = $q.defer();
openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
function success(widgetItem) {
if (!validateImportedWidget(widgetItem)) {
toast.showError($translate.instant('dashboard.invalid-widget-file-error'));
+ deferred.reject();
} else {
var widget = widgetItem.widget;
- var aliasesInfo = widgetItem.aliasesInfo;
+ var aliasesInfo = prepareAliasesInfo(widgetItem.aliasesInfo);
var originalColumns = widgetItem.originalColumns;
+ var originalSize = widgetItem.originalSize;
var datasourceAliases = aliasesInfo.datasourceAliases;
var targetDeviceAliases = aliasesInfo.targetDeviceAliases;
if (datasourceAliases || targetDeviceAliases) {
- var deviceAliases = {};
+ var entityAliases = {};
var datasourceAliasesMap = {};
var targetDeviceAliasesMap = {};
var aliasId = 1;
@@ -378,56 +410,67 @@ 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);
+ addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
},
- function fail() {}
+ function fail() {
+ deferred.reject();
+ }
);
} else {
- addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+ addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
}
}
);
} else {
- addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+ addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
}
} else {
- addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
+ addImportedWidget(dashboard, targetState, targetLayoutFunction, $event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred);
}
}
},
- function fail() {}
+ function fail() {
+ deferred.reject();
+ }
);
+ return deferred.promise;
}
function validateImportedWidget(widgetItem) {
@@ -446,8 +489,26 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
- function addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns) {
- itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, -1, -1);
+ function addImportedWidget(dashboard, targetState, targetLayoutFunction, event, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, deferred) {
+ targetLayoutFunction(event).then(
+ function success(targetLayout) {
+ itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget,
+ aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then(
+ function() {
+ deferred.resolve(
+ {
+ widget: widget,
+ layoutId: targetLayout
+ }
+ );
+ }
+ );
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
}
// Dashboard functions
@@ -477,18 +538,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 +596,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 +651,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();
});
ui/src/app/layout/index.js 4(+4 -0)
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index 32395a6..be63558 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,
ui/src/app/locale/locale.constant.js 157(+148 -9)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 549f4c8..7f52eac 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -14,7 +14,10 @@
* limitations under the License.
*/
+import ThingsboardMissingTranslateHandler from './translate-handler';
+
export default angular.module('thingsboard.locale', [])
+ .factory('tbMissingTranslationHandler', ThingsboardMissingTranslateHandler)
.constant('locales',
{
'en_US': {
@@ -62,6 +65,8 @@ export default angular.module('thingsboard.locale', [])
"undo": "Undo",
"copy": "Copy",
"paste": "Paste",
+ "copy-reference": "Copy reference",
+ "paste-reference": "Paste reference",
"import": "Import",
"export": "Export",
"share-via": "Share via {{provider}}"
@@ -101,6 +106,56 @@ export default angular.module('thingsboard.locale', [])
"enable-tls": "Enable TLS",
"send-test-mail": "Send test mail"
},
+ "asset": {
+ "asset": "Asset",
+ "assets": "Assets",
+ "management": "Asset management",
+ "view-assets": "View Assets",
+ "add": "Add Asset",
+ "assign-to-customer": "Assign to customer",
+ "assign-asset-to-customer": "Assign Asset(s) To Customer",
+ "assign-asset-to-customer-text": "Please select the assets to assign to the customer",
+ "no-assets-text": "No assets found",
+ "assign-to-customer-text": "Please select the customer to assign the asset(s)",
+ "public": "Public",
+ "assignedToCustomer": "Assigned to customer",
+ "make-public": "Make asset public",
+ "make-private": "Make asset private",
+ "unassign-from-customer": "Unassign from customer",
+ "delete": "Delete asset",
+ "asset-public": "Asset is public",
+ "name": "Name",
+ "name-required": "Name is required.",
+ "description": "Description",
+ "type": "Type",
+ "type-required": "Type is required.",
+ "details": "Details",
+ "events": "Events",
+ "add-asset-text": "Add new asset",
+ "asset-details": "Asset details",
+ "assign-assets": "Assign assets",
+ "assign-assets-text": "Assign { count, select, 1 {1 asset} other {# assets} } to customer",
+ "delete-assets": "Delete assets",
+ "unassign-assets": "Unassign assets",
+ "unassign-assets-action-title": "Unassign { count, select, 1 {1 asset} other {# assets} } from customer",
+ "assign-new-asset": "Assign new asset",
+ "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?",
+ "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.",
+ "delete-assets-title": "Are you sure you want to delete { count, select, 1 {1 asset} other {# assets} }?",
+ "delete-assets-action-title": "Delete { count, select, 1 {1 asset} other {# assets} }",
+ "delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.",
+ "make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?",
+ "make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.",
+ "make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?",
+ "make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.",
+ "unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?",
+ "unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.",
+ "unassign-asset": "Unassign asset",
+ "unassign-assets-title": "Are you sure you want to unassign { count, select, 1 {1 asset} other {# assets} }?",
+ "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.",
+ "copyId": "Copy asset Id",
+ "idCopiedMessage": "Asset Id has been copied to clipboard"
+ },
"attribute": {
"attributes": "Attributes",
"latest-telemetry": "Latest telemetry",
@@ -157,8 +212,10 @@ export default angular.module('thingsboard.locale', [])
"dashboard": "Customer Dashboard",
"dashboards": "Customer Dashboards",
"devices": "Customer Devices",
+ "assets": "Customer Assets",
"public-dashboards": "Public Dashboards",
"public-devices": "Public Devices",
+ "public-assets": "Public Assets",
"add": "Add Customer",
"delete": "Delete customer",
"manage-customer-users": "Manage customer users",
@@ -166,6 +223,8 @@ export default angular.module('thingsboard.locale', [])
"manage-customer-dashboards": "Manage customer dashboards",
"manage-public-devices": "Manage public devices",
"manage-public-dashboards": "Manage public dashboards",
+ "manage-customer-assets": "Manage customer assets",
+ "manage-public-assets": "Manage public assets",
"add-customer-text": "Add new customer",
"no-customers-text": "No customers found",
"customer-details": "Customer details",
@@ -175,11 +234,16 @@ export default angular.module('thingsboard.locale', [])
"delete-customers-action-title": "Delete { count, select, 1 {1 customer} other {# customers} }",
"delete-customers-text": "Be careful, after the confirmation all selected customers will be removed and all related data will become unrecoverable.",
"manage-users": "Manage users",
+ "manage-assets": "Manage assets",
"manage-devices": "Manage devices",
"manage-dashboards": "Manage dashboards",
"title": "Title",
"title-required": "Title is required.",
- "description": "Description"
+ "description": "Description",
+ "details": "Details",
+ "events": "Events",
+ "copyId": "Copy customer Id",
+ "idCopiedMessage": "Customer Id has been copied to clipboard"
},
"datetime": {
"date-from": "Date from",
@@ -265,7 +329,8 @@ export default angular.module('thingsboard.locale', [])
"max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
"display-title": "Display dashboard title",
"title-color": "Title color",
- "display-device-selection": "Display device selection",
+ "display-dashboards-selection": "Display dashboards selection",
+ "display-entities-selection": "Display entities selection",
"display-dashboard-timewindow": "Display timewindow",
"display-dashboard-export": "Display export",
"import": "Import dashboard",
@@ -291,7 +356,29 @@ export default angular.module('thingsboard.locale', [])
"public": "Public",
"public-link": "Public link",
"copy-public-link": "Copy public link",
- "public-link-copied-message": "Dashboard public link has been copied to clipboard"
+ "public-link-copied-message": "Dashboard public link has been copied to clipboard",
+ "manage-states": "Manage dashboard states",
+ "states": "Dashboard states",
+ "search-states": "Search dashboard states",
+ "selected-states": "{ count, select, 1 {1 dashboard state} other {# dashboard states} } selected",
+ "edit-state": "Edit dashboard state",
+ "delete-state": "Delete dashboard state",
+ "add-state": "Add dashboard state",
+ "state": "Dashboard state",
+ "state-name": "Name",
+ "state-name-required": "Dashboard state name is required.",
+ "state-name-exists": "Dashboard state with the same name is already exists.",
+ "state-id": "State Id",
+ "state-id-required": "Dashboard state id is required.",
+ "state-id-exists": "Dashboard state with the same id is already exists.",
+ "invalid-state-id-format": "Only alphanumeric characters and underscore are allowed.",
+ "is-root-state": "Root state",
+ "delete-state-title": "Delete dashboard state",
+ "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?",
+ "show-details": "Show details",
+ "hide-details": "Hide details",
+ "select-state": "Select target state",
+ "state-controller": "State controller"
},
"datakey": {
"settings": "Settings",
@@ -303,10 +390,10 @@ export default angular.module('thingsboard.locale', [])
"configuration": "Data key configuration",
"timeseries": "Timeseries",
"attributes": "Attributes",
- "timeseries-required": "Device timeseries is required.",
- "timeseries-or-attributes-required": "Device timeseries/attributes is required.",
+ "timeseries-required": "Entity timeseries are required.",
+ "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
"function-types": "Function types",
- "function-types-required": "Function types is required."
+ "function-types-required": "Function types are required."
},
"datasource": {
"type": "Datasource type",
@@ -411,6 +498,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):<br/>{{widgetsList}}",
+ "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>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",
@@ -475,6 +597,15 @@ export default angular.module('thingsboard.locale', [])
"no-return-error": "Function must return value!",
"return-type-mismatch": "Function must return value of '{{type}}' type!"
},
+ "layout": {
+ "layout": "Layout",
+ "manage": "Manage layouts",
+ "settings": "Layout settings",
+ "color": "Color",
+ "main": "Main",
+ "right": "Right",
+ "select": "Select target layout"
+ },
"legend": {
"position": "Legend position",
"show-max": "Show max value",
@@ -541,7 +672,9 @@ export default angular.module('thingsboard.locale', [])
"export-failed-error": "Unable to export plugin: {{error}}",
"create-new-plugin": "Create new plugin",
"plugin-file": "Plugin file",
- "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure."
+ "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
+ "copyId": "Copy plugin Id",
+ "idCopiedMessage": "Plugin Id has been copied to clipboard"
},
"position": {
"top": "Top",
@@ -604,7 +737,9 @@ export default angular.module('thingsboard.locale', [])
"export-failed-error": "Unable to export rule: {{error}}",
"create-new-rule": "Create new rule",
"rule-file": "Rule file",
- "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure."
+ "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure.",
+ "copyId": "Copy rule Id",
+ "idCopiedMessage": "Rule Id has been copied to clipboard"
},
"rule-plugin": {
"management": "Rules and plugins management"
@@ -626,7 +761,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} }",
ui/src/app/locale/locale.constant-zh.js 150(+75 -75)
diff --git a/ui/src/app/locale/locale.constant-zh.js b/ui/src/app/locale/locale.constant-zh.js
index 56e4d57..ba8e3c0 100644
--- a/ui/src/app/locale/locale.constant-zh.js
+++ b/ui/src/app/locale/locale.constant-zh.js
@@ -116,12 +116,12 @@ export default function addLocaleChinese(locales) {
"delete-attributes-text" : "注意,确认后所有选中的属性都会被删除。",
"delete-attributes" : "删除属性",
"enter-attribute-value" : "输入属性值",
- "show-on-widget" : "在小部件上显示",
- "widget-mode" : "小部件模式",
- "next-widget" : "下一个小部件",
- "prev-widget" : "上一个小部件",
+ "show-on-widget" : "在部件上显示",
+ "widget-mode" : "部件模式",
+ "next-widget" : "下一个部件",
+ "prev-widget" : "上一个部件",
"add-to-dashboard" : "添加到仪表板",
- "add-widget-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} } 被选中"
},
@@ -169,7 +169,7 @@ export default function addLocaleChinese(locales) {
"customer-details" : "客户详情",
"delete-customer-title" : "您确定要删除客户'{{customerTitle}}'吗?",
"delete-customer-text" : "小心!确认后,客户及其所有相关数据将不可恢复。",
- "delete-customers-title" : "您确定要删除 { count, select, 1 {1 customer} other {# customers} }?吗",
+ "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" : "管理用户",
@@ -199,11 +199,11 @@ export default function addLocaleChinese(locales) {
"make-public" : "使仪表板公有",
"make-private" : "使仪表板私有",
"no-dashboards-text" : "没有找到仪表板",
- "no-widgets" : "没有配置小部件",
- "add-widget" : "添加新的小部件",
+ "no-widgets" : "没有配置部件",
+ "add-widget" : "添加新的部件",
"title" : "标题",
- "select-widget-title" : "选择小部件",
- "select-widget-subtitle" : "可用的小部件类型列表",
+ "select-widget-title" : "选择部件",
+ "select-widget-subtitle" : "可用的部件类型列表",
"delete" : "删除仪表板",
"title-required" : "需要标题。",
"description" : "描述",
@@ -273,11 +273,11 @@ export default function addLocaleChinese(locales) {
"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" : "配置导入的窗口小部件使用的别名",
+ "create-new-widget" : "创建新部件",
+ "import-widget" : "导入部件",
+ "widget-file" : "部件文件",
+ "invalid-widget-file-error" : "无法导入窗口部件: 窗口部件数据结构无效。",
+ "widget-import-missing-aliases-title" : "配置导入的窗口部件使用的别名",
"open-toolbar" : "打开仪表板工具栏",
"close-toolbar" : "关闭工具栏",
"configuration-error" : "配置错误",
@@ -694,34 +694,34 @@ export default function addLocaleChinese(locales) {
"true" : "真"
},
"widget" : {
- "widget-library" : "小部件库",
- "widget-bundle" : "小部件包",
- "select-widgets-bundle" : "选择小部件包",
- "management" : "小部件管理",
- "editor" : "小部件编辑器",
- "widget-type-not-found" : "加载小部件配置时出现问题。<br> 可能关联的\n 小部件类型已删除。",
- "widget-type-load-error" : "由于以下错误,小工具未加载:",
- "remove" : "删除小部件",
- "edit" : "编辑小部件",
- "remove-widget-title" : "您确定要删除小部件 '{{widgetTitle}}' 吗?",
- "remove-widget-text" : "确认后,窗口小部件和所有相关数据将不可恢复。",
+ "widget-library" : "部件库",
+ "widget-bundle" : "部件包",
+ "select-widgets-bundle" : "选择部件包",
+ "management" : "部件管理",
+ "editor" : "部件编辑器",
+ "widget-type-not-found" : "加载部件配置时出现问题。<br> 可能关联的\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" : "请输入新的小部件标题和/或选择目标小部件包",
+ "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" : "小部件类型",
+ "run" : "运行部件",
+ "title" : "部件标题",
+ "title-required" : "需要部件标题。",
+ "type" : "部件类型",
"resources" : "资源",
"resource-url" : "JavaScript/CSS URL",
"remove-resource" : "删除资源",
@@ -732,42 +732,42 @@ export default function addLocaleChinese(locales) {
"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" : "导出小部件"
+ "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" : "删除小部件包",
+ "widgets-bundles" : "部件包",
+ "add" : "添加部件包",
+ "delete" : "删除部件包",
"title" : "标题",
"title-required" : "标题是必填项。",
- "add-widgets-bundle-text" : "添加新的小部件包",
- "no-widgets-bundles-text" : "找不到小部件包",
- "empty" : "小部件包是空的",
+ "add-widgets-bundle-text" : "添加新的部件包",
+ "no-widgets-bundles-text" : "找不到部件包",
+ "empty" : "部件包是空的",
"details" : "详情",
- "widgets-bundle-details" : "小部件包详细信息",
- "delete-widgets-bundle-title" : "您确定要删除小部件包 '{{widgetsBundleTitle}}'吗?",
- "delete-widgets-bundle-text" : "小心!确认后,小部件包和所有相关数据将不可恢复。",
+ "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" : "需要小部件包。",
+ "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" : "无法导入小部件包:无效的小部件包数据结构。"
+ "import" : "导入部件包",
+ "export" : "导出部件包",
+ "export-failed-error" : "无法导出部件包: {{error}}",
+ "create-new-widgets-bundle" : "创建新的部件包",
+ "widgets-bundle-file" : "部件包文件",
+ "invalid-widgets-bundle-file-error" : "无法导入部件包:无效的部件包数据结构。"
},
"widget-config" : {
"data" : "数据",
@@ -798,12 +798,12 @@ export default function addLocaleChinese(locales) {
"target-device" : "目标设备"
},
"widget-type" : {
- "import" : "导入小部件类型",
- "export" : "导出小部件类型",
- "export-failed-error" : "无法导出小部件类型: {{error}}",
- "create-new-widget-type" : "创建新的小部件类型",
- "widget-type-file" : "小部件类型文件",
- "invalid-widget-type-file-error" : "无法导入小部件类型:无效的小部件类型数据结构。"
+ "import" : "导入部件类型",
+ "export" : "导出部件类型",
+ "export-failed-error" : "无法导出部件类型: {{error}}",
+ "create-new-widget-type" : "创建新的部件类型",
+ "widget-type-file" : "部件类型文件",
+ "invalid-widget-type-file-error" : "无法导入部件类型:无效的部件类型数据结构。"
},
"language" : {
"language" : "语言",
@@ -811,10 +811,10 @@ export default function addLocaleChinese(locales) {
"ko_KR" : "韩语",
"zh_CN" : "汉语",
"ru_RU" : "俄语",
- "es_ES": "西班牙語"
+ "es_ES": "西班牙语"
}
};
angular.extend(locales, {
'zh_CN' : zh_CN
});
-}
\ No newline at end of file
+}
ui/src/app/locale/translate-handler.js 26(+26 -0)
diff --git a/ui/src/app/locale/translate-handler.js b/ui/src/app/locale/translate-handler.js
new file mode 100644
index 0000000..d227041
--- /dev/null
+++ b/ui/src/app/locale/translate-handler.js
@@ -0,0 +1,26 @@
+/*
+ * 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 ThingsboardMissingTranslateHandler($log, types) {
+
+ return function (translationId) {
+ if (translationId && !translationId.startsWith(types.translate.dashboardStatePrefix)) {
+ $log.warn('Translation for ' + translationId + ' doesn\'t exist');
+ }
+ };
+
+}
\ No newline at end of file
diff --git a/ui/src/app/plugin/plugin.directive.js b/ui/src/app/plugin/plugin.directive.js
index d1fdeea..caaa7fa 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;
ui/src/app/plugin/plugin-fieldset.tpl.html 10(+10 -0)
diff --git a/ui/src/app/plugin/plugin-fieldset.tpl.html b/ui/src/app/plugin/plugin-fieldset.tpl.html
index 86dfeb5..b5f8742 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 }}</md-button>
+<div layout="row">
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onPluginIdCopied(e)"
+ data-clipboard-text="{{plugin.id.id}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>plugin.copyId</span>
+ </md-button>
+</div>
+
<md-content class="md-padding" layout="column" style="overflow-x: hidden">
<fieldset ng-disabled="loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
ui/src/app/plugin/plugins.tpl.html 17(+17 -0)
diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html
index 04a7a09..bd8cf74 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)"></tb-plugin>
</md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.plugin}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.plugin}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+ disable-attribute-scope-selection="true">
+ </tb-attribute-table>
+ </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'plugin.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.plugin"
entity-id="vm.grid.operatingItem().id.id"
diff --git a/ui/src/app/profile/profile.controller.js b/ui/src/app/profile/profile.controller.js
index cea51be..187a9d2 100644
--- a/ui/src/app/profile/profile.controller.js
+++ b/ui/src/app/profile/profile.controller.js
@@ -31,7 +31,7 @@ export default function ProfileController(userService, $scope, $document, $mdDia
en_US: {value : "en_US", name: "language.en_US"},
ko_KR: {value : "ko_KR", name: "language.ko_KR"},
zh_CN: {value : "zh_CN", name: "language.zh_CN"},
- ru_RU: {value : "ru_RU", name: "language.ru_RU"}ñ
+ ru_RU: {value : "ru_RU", name: "language.ru_RU"},
es_ES: {value : "es_ES", name: "language.es_ES"},
};
ui/src/app/rule/rule.directive.js 7(+6 -1)
diff --git a/ui/src/app/rule/rule.directive.js b/ui/src/app/rule/rule.directive.js
index bd1ab40..bfd6b4c 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) {
ui/src/app/rule/rule-fieldset.tpl.html 10(+10 -0)
diff --git a/ui/src/app/rule/rule-fieldset.tpl.html b/ui/src/app/rule/rule-fieldset.tpl.html
index 0b6508b..0646b3f 100644
--- a/ui/src/app/rule/rule-fieldset.tpl.html
+++ b/ui/src/app/rule/rule-fieldset.tpl.html
@@ -28,6 +28,16 @@
ng-show="!isEdit && !isReadOnly"
class="md-raised md-primary">{{ 'rule.delete' | translate }}</md-button>
+<div layout="row">
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onRuleIdCopied(e)"
+ data-clipboard-text="{{rule.id.id}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>rule.copyId</span>
+ </md-button>
+</div>
+
<md-content class="md-padding tb-rule" layout="column">
<fieldset ng-disabled="loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
ui/src/app/rule/rules.tpl.html 17(+17 -0)
diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html
index 8aca1e3..bd7ea48 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)"></tb-rule>
</md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.rule}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.rule}}"
+ entity-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+ disable-attribute-scope-selection="true">
+ </tb-attribute-table>
+ </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'rule.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.rule"
entity-id="vm.grid.operatingItem().id.id"
ui/src/app/services/item-buffer.service.js 272(+173 -99)
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 0ac71f5..afb63bf 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -24,15 +24,19 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
.name;
/*@ngInject*/
-function ItemBuffer(bufferStore, types) {
+function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
const WIDGET_ITEM = "widget_item";
+ const WIDGET_REFERENCE = "widget_reference";
var service = {
prepareWidgetItem: prepareWidgetItem,
copyWidget: copyWidget,
+ copyWidgetReference: copyWidgetReference,
hasWidget: hasWidget,
+ canPasteWidgetReference: canPasteWidgetReference,
pasteWidget: pasteWidget,
+ pasteWidgetReference: pasteWidgetReference,
addWidgetToDashboard: addWidgetToDashboard
}
@@ -43,58 +47,75 @@ function ItemBuffer(bufferStore, types) {
datasourceAliases: {
datasourceIndex: {
aliasName: "...",
- deviceFilter: "..."
+ entityType: "...",
+ entityFilter: "..."
}
}
targetDeviceAliases: {
targetDeviceAliasIndex: {
aliasName: "...",
- deviceFilter: "..."
+ entityType: "...",
+ entityFilter: "..."
}
}
....
}
**/
- function getDeviceFilter(alias) {
- if (alias.deviceId) {
- return {
- useFilter: false,
- deviceNameFilter: '',
- deviceList: [alias.deviceId]
- };
- } else {
- return alias.deviceFilter;
+ function prepareAliasInfo(entityAlias) {
+ return {
+ aliasName: entityAlias.alias,
+ entityType: entityAlias.entityType,
+ entityFilter: entityAlias.entityFilter
+ };
+ }
+
+ function getOriginalColumns(dashboard, sourceState, sourceLayout) {
+ var originalColumns = 24;
+ var gridSettings = null;
+ var state = dashboard.configuration.states[sourceState];
+ var layoutCount = Object.keys(state.layouts).length;
+ if (state) {
+ var layout = state.layouts[sourceLayout];
+ if (layout) {
+ gridSettings = layout.gridSettings;
+
+ }
+ }
+ if (gridSettings &&
+ gridSettings.columns) {
+ originalColumns = gridSettings.columns;
}
+ originalColumns = originalColumns * layoutCount;
+ return originalColumns;
}
- function prepareAliasInfo(deviceAlias) {
+ function getOriginalSize(dashboard, sourceState, sourceLayout, widget) {
+ var layout = dashboard.configuration.states[sourceState].layouts[sourceLayout];
+ var widgetLayout = layout.widgets[widget.id];
return {
- aliasName: deviceAlias.alias,
- deviceFilter: getDeviceFilter(deviceAlias)
- };
+ sizeX: widgetLayout.sizeX,
+ sizeY: widgetLayout.sizeY
+ }
}
- function prepareWidgetItem(dashboard, widget) {
+ function prepareWidgetItem(dashboard, sourceState, sourceLayout, widget) {
var aliasesInfo = {
datasourceAliases: {},
targetDeviceAliases: {}
};
- var originalColumns = 24;
- if (dashboard.configuration.gridSettings &&
- dashboard.configuration.gridSettings.columns) {
- originalColumns = dashboard.configuration.gridSettings.columns;
- }
+ var originalColumns = getOriginalColumns(dashboard, sourceState, sourceLayout);
+ var originalSize = getOriginalSize(dashboard, sourceState, sourceLayout, widget);
if (widget.config && dashboard.configuration
- && dashboard.configuration.deviceAliases) {
- var deviceAlias;
+ && dashboard.configuration.entityAliases) {
+ var entityAlias;
if (widget.config.datasources) {
for (var i=0;i<widget.config.datasources.length;i++) {
var datasource = widget.config.datasources[i];
- if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
- deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
- if (deviceAlias) {
- aliasesInfo.datasourceAliases[i] = prepareAliasInfo(deviceAlias);
+ if (datasource.type === types.datasourceType.entity && datasource.entityAliasId) {
+ entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId];
+ if (entityAlias) {
+ aliasesInfo.datasourceAliases[i] = prepareAliasInfo(entityAlias);
}
}
}
@@ -103,9 +124,9 @@ function ItemBuffer(bufferStore, types) {
for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
if (targetDeviceAliasId) {
- deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
- if (deviceAlias) {
- aliasesInfo.targetDeviceAliases[i] = prepareAliasInfo(deviceAlias);
+ entityAlias = dashboard.configuration.entityAliases[targetDeviceAliasId];
+ if (entityAlias) {
+ aliasesInfo.targetDeviceAliases[i] = prepareAliasInfo(entityAlias);
}
}
}
@@ -114,142 +135,197 @@ function ItemBuffer(bufferStore, types) {
return {
widget: widget,
aliasesInfo: aliasesInfo,
+ originalSize: originalSize,
originalColumns: originalColumns
- }
+ };
+ }
+
+ function prepareWidgetReference(dashboard, sourceState, sourceLayout, widget) {
+ var originalColumns = getOriginalColumns(dashboard, sourceState, sourceLayout);
+ var originalSize = getOriginalSize(dashboard, sourceState, sourceLayout, widget);
+
+ return {
+ dashboardId: dashboard.id.id,
+ sourceState: sourceState,
+ sourceLayout: sourceLayout,
+ widgetId: widget.id,
+ originalSize: originalSize,
+ originalColumns: originalColumns
+ };
}
- function copyWidget(dashboard, widget) {
- var widgetItem = prepareWidgetItem(dashboard, widget);
+ function copyWidget(dashboard, sourceState, sourceLayout, widget) {
+ var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
}
+ function copyWidgetReference(dashboard, sourceState, sourceLayout, widget) {
+ var widgetReference = prepareWidgetReference(dashboard, sourceState, sourceLayout, widget);
+ bufferStore.set(WIDGET_REFERENCE, angular.toJson(widgetReference));
+ }
+
function hasWidget() {
return bufferStore.get(WIDGET_ITEM);
}
- function pasteWidget(targetDashboard, position, onAliasesUpdate) {
+ function canPasteWidgetReference(dashboard, state, layout) {
+ var widgetReferenceJson = bufferStore.get(WIDGET_REFERENCE);
+ if (widgetReferenceJson) {
+ var widgetReference = angular.fromJson(widgetReferenceJson);
+ if (widgetReference.dashboardId === dashboard.id.id) {
+ if ((widgetReference.sourceState != state || widgetReference.sourceLayout != layout)
+ && dashboard.configuration.widgets[widgetReference.widgetId]) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ function pasteWidgetReference(targetDashboard, targetState, targetLayout, position) {
+ var deferred = $q.defer();
+ var widgetReferenceJson = bufferStore.get(WIDGET_REFERENCE);
+ if (widgetReferenceJson) {
+ var widgetReference = angular.fromJson(widgetReferenceJson);
+ var widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
+ if (widget) {
+ var originalColumns = widgetReference.originalColumns;
+ var originalSize = widgetReference.originalSize;
+ var targetRow = -1;
+ var targetColumn = -1;
+ if (position) {
+ targetRow = position.row;
+ targetColumn = position.column;
+ }
+ addWidgetToDashboard(targetDashboard, targetState, targetLayout, widget, null,
+ null, originalColumns, originalSize, targetRow, targetColumn).then(
+ function () {
+ deferred.resolve(widget);
+ }
+ );
+ } else {
+ deferred.reject();
+ }
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+
+ function pasteWidget(targetDashboard, targetState, targetLayout, position, onAliasesUpdateFunction) {
+ var deferred = $q.defer();
var widgetItemJson = bufferStore.get(WIDGET_ITEM);
if (widgetItemJson) {
var widgetItem = angular.fromJson(widgetItemJson);
var widget = widgetItem.widget;
var aliasesInfo = widgetItem.aliasesInfo;
var originalColumns = widgetItem.originalColumns;
+ var originalSize = widgetItem.originalSize;
var targetRow = -1;
var targetColumn = -1;
if (position) {
targetRow = position.row;
targetColumn = position.column;
}
- addWidgetToDashboard(targetDashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, targetRow, targetColumn);
+ widget.id = utils.guid();
+ addWidgetToDashboard(targetDashboard, targetState, targetLayout, widget, aliasesInfo,
+ onAliasesUpdateFunction, originalColumns, originalSize, targetRow, targetColumn).then(
+ function () {
+ deferred.resolve(widget);
+ }
+ );
+ } else {
+ deferred.reject();
}
+ return deferred.promise;
}
- function addWidgetToDashboard(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns, row, column) {
+ function addWidgetToDashboard(dashboard, targetState, targetLayout, widget, aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, row, column) {
+ var deferred = $q.defer();
var theDashboard;
if (dashboard) {
theDashboard = dashboard;
} else {
theDashboard = {};
}
- if (!theDashboard.configuration) {
- theDashboard.configuration = {};
- }
- if (!theDashboard.configuration.deviceAliases) {
- theDashboard.configuration.deviceAliases = {};
- }
- var newDeviceAliases = updateAliases(theDashboard, widget, aliasesInfo);
- if (!theDashboard.configuration.widgets) {
- theDashboard.configuration.widgets = [];
- }
- var targetColumns = 24;
- if (theDashboard.configuration.gridSettings &&
- theDashboard.configuration.gridSettings.columns) {
- targetColumns = theDashboard.configuration.gridSettings.columns;
- }
- if (targetColumns != originalColumns) {
- var ratio = targetColumns / originalColumns;
- widget.sizeX *= ratio;
- widget.sizeY *= ratio;
- }
- if (row > -1 && column > - 1) {
- widget.row = row;
- widget.col = column;
- } else {
- row = 0;
- for (var w in theDashboard.configuration.widgets) {
- var existingWidget = theDashboard.configuration.widgets[w];
- var wRow = existingWidget.row ? existingWidget.row : 0;
- var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
- var bottom = wRow + wSizeY;
- row = Math.max(row, bottom);
+ theDashboard = dashboardUtils.validateAndUpdateDashboard(theDashboard);
+
+ var callAliasUpdateFunction = false;
+ if (aliasesInfo) {
+ var newEntityAliases = updateAliases(theDashboard, widget, aliasesInfo);
+ var aliasesUpdated = !angular.equals(newEntityAliases, theDashboard.configuration.entityAliases);
+ if (aliasesUpdated) {
+ theDashboard.configuration.entityAliases = newEntityAliases;
+ if (onAliasesUpdateFunction) {
+ callAliasUpdateFunction = true;
+ }
}
- widget.row = row;
- widget.col = 0;
}
- var aliasesUpdated = !angular.equals(newDeviceAliases, theDashboard.configuration.deviceAliases);
- if (aliasesUpdated) {
- theDashboard.configuration.deviceAliases = newDeviceAliases;
- if (onAliasesUpdate) {
- onAliasesUpdate();
- }
+ dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column);
+ if (callAliasUpdateFunction) {
+ onAliasesUpdateFunction().then(
+ function() {
+ deferred.resolve(theDashboard);
+ }
+ );
+ } else {
+ deferred.resolve(theDashboard);
}
- theDashboard.configuration.widgets.push(widget);
- return theDashboard;
+ return deferred.promise;
}
function updateAliases(dashboard, widget, aliasesInfo) {
- var deviceAliases = angular.copy(dashboard.configuration.deviceAliases);
+ var entityAliases = angular.copy(dashboard.configuration.entityAliases);
var aliasInfo;
var newAliasId;
for (var datasourceIndex in aliasesInfo.datasourceAliases) {
aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex];
- newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
- widget.config.datasources[datasourceIndex].deviceAliasId = newAliasId;
+ newAliasId = getEntityAliasId(entityAliases, aliasInfo);
+ widget.config.datasources[datasourceIndex].entityAliasId = newAliasId;
}
for (var targetDeviceAliasIndex in aliasesInfo.targetDeviceAliases) {
aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex];
- newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
+ newAliasId = getEntityAliasId(entityAliases, aliasInfo);
widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
}
- return deviceAliases;
+ return entityAliases;
}
- function isDeviceFiltersEqual(alias1, alias2) {
- var filter1 = getDeviceFilter(alias1);
- var filter2 = getDeviceFilter(alias2);
- return angular.equals(filter1, filter2);
+ function isEntityAliasEqual(alias1, alias2) {
+ return alias1.entityType === alias2.entityType &&
+ angular.equals(alias1.entityFilter, alias2.entityFilter);
}
- function getDeviceAliasId(deviceAliases, aliasInfo) {
+ function getEntityAliasId(entityAliases, aliasInfo) {
var newAliasId;
- for (var aliasId in deviceAliases) {
- if (isDeviceFiltersEqual(deviceAliases[aliasId], aliasInfo)) {
+ for (var aliasId in entityAliases) {
+ if (isEntityAliasEqual(entityAliases[aliasId], aliasInfo)) {
newAliasId = aliasId;
break;
}
}
if (!newAliasId) {
- var newAliasName = createDeviceAliasName(deviceAliases, aliasInfo.aliasName);
+ var newAliasName = createEntityAliasName(entityAliases, aliasInfo.aliasName);
newAliasId = 0;
- for (aliasId in deviceAliases) {
+ for (aliasId in entityAliases) {
newAliasId = Math.max(newAliasId, aliasId);
}
newAliasId++;
- deviceAliases[newAliasId] = {alias: newAliasName, deviceFilter: aliasInfo.deviceFilter};
+ entityAliases[newAliasId] = {alias: newAliasName, entityType: aliasInfo.entityType, entityFilter: aliasInfo.entityFilter};
}
return newAliasId;
}
- function createDeviceAliasName(deviceAliases, alias) {
+ function createEntityAliasName(entityAliases, alias) {
var c = 0;
var newAlias = angular.copy(alias);
var unique = false;
while (!unique) {
unique = true;
- for (var devAliasId in deviceAliases) {
- var devAlias = deviceAliases[devAliasId];
- if (newAlias === devAlias.alias) {
+ for (var entAliasId in entityAliases) {
+ var entAlias = entityAliases[entAliasId];
+ if (newAlias === entAlias.alias) {
c++;
newAlias = alias + c;
unique = false;
@@ -258,6 +334,4 @@ function ItemBuffer(bufferStore, types) {
}
return newAlias;
}
-
-
}
\ No newline at end of file
ui/src/app/services/menu.service.js 32(+32 -0)
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index d0d1596..24db768 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -189,6 +189,12 @@ function Menu(userService, $state, $rootScope) {
icon: 'supervisor_account'
},
{
+ name: 'asset.assets',
+ type: 'link',
+ state: 'home.assets',
+ icon: 'domain'
+ },
+ {
name: 'device.devices',
type: 'link',
state: 'home.devices',
@@ -234,6 +240,16 @@ function Menu(userService, $state, $rootScope) {
]
},
{
+ name: 'asset.management',
+ places: [
+ {
+ name: 'asset.assets',
+ icon: 'domain',
+ state: 'home.assets'
+ }
+ ]
+ },
+ {
name: 'device.management',
places: [
{
@@ -268,6 +284,12 @@ function Menu(userService, $state, $rootScope) {
icon: 'home'
},
{
+ name: 'asset.assets',
+ type: 'link',
+ state: 'home.assets',
+ icon: 'domain'
+ },
+ {
name: 'device.devices',
type: 'link',
state: 'home.devices',
@@ -282,6 +304,16 @@ function Menu(userService, $state, $rootScope) {
homeSections =
[{
+ name: 'asset.view-assets',
+ places: [
+ {
+ name: 'asset.assets',
+ icon: 'domain',
+ state: 'home.assets'
+ }
+ ]
+ },
+ {
name: 'device.view-devices',
places: [
{
diff --git a/ui/src/app/tenant/tenant.controller.js b/ui/src/app/tenant/tenant.controller.js
index 3ba3b4b..93fc982 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 13a7eba..7876061 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 {
ui/src/app/tenant/tenant-fieldset.tpl.html 10(+10 -0)
diff --git a/ui/src/app/tenant/tenant-fieldset.tpl.html b/ui/src/app/tenant/tenant-fieldset.tpl.html
index 3a6d9e6..76b0c3f 100644
--- a/ui/src/app/tenant/tenant-fieldset.tpl.html
+++ b/ui/src/app/tenant/tenant-fieldset.tpl.html
@@ -18,6 +18,16 @@
<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'tenant.manage-tenant-admins' | translate }}</md-button>
<md-button ng-click="onDeleteTenant({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'tenant.delete' | translate }}</md-button>
+<div layout="row">
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onTenantIdCopied(e)"
+ data-clipboard-text="{{tenant.id.id}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>tenant.copyId</span>
+ </md-button>
+</div>
+
<md-content class="md-padding" layout="column">
<fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
ui/src/app/tenant/tenants.tpl.html 40(+35 -5)
diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html
index 704ec0b..ada2a3f 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.
-->
+
<tb-grid grid-configuration="vm.tenantGridConfig">
<details-buttons tb-help="'tenants'" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
- <tb-tenant tenant="vm.grid.operatingItem()"
- is-edit="vm.grid.detailsConfig.isDetailsEditMode"
- the-form="vm.grid.detailsForm"
- on-manage-users="vm.openTenantUsers(event, vm.grid.detailsConfig.currentItem)"
- on-delete-tenant="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-tenant>
+ <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'tenant.details' | translate }}">
+ <tb-tenant tenant="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ the-form="vm.grid.detailsForm"
+ on-manage-users="vm.openTenantUsers(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-tenant="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-tenant>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.tenant}}"
+ entity-name="vm.grid.operatingItem().title"
+ default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <tb-attribute-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-type="{{vm.types.entityType.tenant}}"
+ entity-name="vm.grid.operatingItem().title"
+ default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+ disable-attribute-scope-selection="true">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'tenant.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.tenant"
+ entity-id="vm.grid.operatingItem().id.id"
+ tenant-id="vm.types.id.nullUid"
+ default-event-type="{{vm.types.eventType.alarm.value}}">
+ </tb-event-table>
+ </md-tab>
+ </md-tabs>
</tb-grid>
ui/src/app/user/user.controller.js 12(+9 -3)
diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js
index eeba2f4..4e702bd 100644
--- a/ui/src/app/user/user.controller.js
+++ b/ui/src/app/user/user.controller.js
@@ -22,7 +22,7 @@ import userCard from './user-card.tpl.html';
/*@ngInject*/
-export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate) {
+export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate, types) {
var tenantId = $stateParams.tenantId;
var customerId = $stateParams.customerId;
@@ -87,7 +87,10 @@ export default function UserController(userService, toast, $scope, $controller,
};
saveUserFunction = function (user) {
user.authority = "TENANT_ADMIN";
- user.tenantId = {id: tenantId};
+ user.tenantId = {
+ entityType: types.entityType.tenant,
+ id: tenantId
+ };
return userService.saveUser(user);
};
refreshUsersParamsFunction = function () {
@@ -100,7 +103,10 @@ export default function UserController(userService, toast, $scope, $controller,
};
saveUserFunction = function (user) {
user.authority = "CUSTOMER_USER";
- user.customerId = {id: customerId};
+ user.customerId = {
+ entityType: types.entityType.customer,
+ id: customerId
+ };
return userService.saveUser(user);
};
refreshUsersParamsFunction = function () {
ui/src/app/user/user.directive.js 4(+4 -0)
diff --git a/ui/src/app/user/user.directive.js b/ui/src/app/user/user.directive.js
index 1a4b069..bd78dc2 100644
--- a/ui/src/app/user/user.directive.js
+++ b/ui/src/app/user/user.directive.js
@@ -28,6 +28,10 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi
var template = $templateCache.get(userFieldsetTemplate);
element.html(template);
+ scope.isTenantAdmin = function() {
+ return scope.user && scope.user.authority === 'TENANT_ADMIN';
+ }
+
scope.isCustomerUser = function() {
return scope.user && scope.user.authority === 'CUSTOMER_USER';
}
ui/src/app/user/user-fieldset.tpl.html 13(+11 -2)
diff --git a/ui/src/app/user/user-fieldset.tpl.html b/ui/src/app/user/user-fieldset.tpl.html
index 7c9b0d6..a2559ff 100644
--- a/ui/src/app/user/user-fieldset.tpl.html
+++ b/ui/src/app/user/user-fieldset.tpl.html
@@ -43,10 +43,19 @@
<label translate>user.description</label>
<textarea ng-model="user.additionalInfo.description" rows="2"></textarea>
</md-input-container>
- <section class="tb-default-dashboard" flex layout="column" ng-show="isCustomerUser()">
+ <section class="tb-default-dashboard" flex layout="column">
<span class="tb-default-dashboard-label" ng-class="{'tb-disabled-label': loading || !isEdit}" translate>user.default-dashboard</span>
<section flex layout="column" layout-gt-sm="row">
- <tb-dashboard-autocomplete flex
+ <tb-dashboard-autocomplete ng-if="isTenantAdmin()"
+ flex
+ ng-disabled="loading || !isEdit"
+ the-form="theForm"
+ ng-model="user.additionalInfo.defaultDashboardId"
+ tenant-id="user.tenantId.id"
+ select-first-dashboard="false">
+ </tb-dashboard-autocomplete>
+ <tb-dashboard-autocomplete ng-if="isCustomerUser()"
+ flex
ng-disabled="loading || !isEdit"
the-form="theForm"
ng-model="user.additionalInfo.defaultDashboardId"
diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js
index 69fb85b..bda8835 100644
--- a/ui/src/app/widget/widget-library.controller.js
+++ b/ui/src/app/widget/widget-library.controller.js
@@ -87,7 +87,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
var sizeX = 8;
var sizeY = Math.floor(widgetTypeInfo.sizeY);
var widget = {
- id: widgetType.id,
+ typeId: widgetType.id,
isSystemType: isSystem,
bundleAlias: bundleAlias,
typeAlias: widgetTypeInfo.alias,
@@ -158,7 +158,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
}
if (widget) {
$state.go('home.widgets-bundles.widget-types.widget-type',
- {widgetTypeId: widget.id.id});
+ {widgetTypeId: widget.typeId.id});
} else {
$mdDialog.show({
controller: 'SelectWidgetTypeController',
@@ -177,7 +177,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
function exportWidgetType(event, widget) {
event.stopPropagation();
- importExport.exportWidgetType(widget.id.id);
+ importExport.exportWidgetType(widget.typeId.id);
}
function importWidgetType($event) {
ui/src/scss/main.scss 18(+17 -1)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index ab9b0d7..4ffabc9 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -280,8 +280,11 @@ $previewSize: 100px;
overflow: hidden;
label {
width: 100%;
- font-size: 24px;
+ font-size: 16px;
text-align: center;
+ @media (min-width: $layout-breakpoint-sm) {
+ font-size: 24px;
+ }
}
}
@@ -369,6 +372,19 @@ md-tabs.tb-headless {
}
}
+.md-button.tb-layout-button {
+ width: 100%;
+ height: 100%;
+ max-width: 240px;
+ span {
+ padding: 40px;
+ font-size: 18px;
+ font-weight: 400;
+ white-space: normal;
+ line-height: 18px;
+ }
+}
+
.md-button.tb-add-new-widget {
border-style: dashed;
border-width: 2px;