Browse Source

Merge branch 'master' into gatling-mqtt

pull/30/head
volodymyr-babak 10 years ago
parent
commit
e1a2df9b47
  1. 2
      application/pom.xml
  2. 2
      application/src/main/conf/logback.xml
  3. 3
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
  4. 26
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  5. 2
      application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
  6. 14
      application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
  7. 3
      application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
  8. 15
      application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
  9. 39
      application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
  10. 2
      application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
  11. 19
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  12. 4
      application/src/main/resources/thingsboard.yml
  13. 108
      application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java
  14. 2
      common/data/pom.xml
  15. 2
      common/message/pom.xml
  16. 38
      common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
  17. 29
      common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
  18. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
  19. 20
      common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
  20. 2
      common/pom.xml
  21. 2
      common/transport/pom.xml
  22. 2
      dao/pom.xml
  23. 16
      dao/src/main/resources/demo-data.cql
  24. 16
      dao/src/main/resources/system-data.cql
  25. 4
      docker/docker-compose.yml
  26. 4
      docker/thingsboard-db-schema/build_and_deploy.sh
  27. 4
      docker/thingsboard/build_and_deploy.sh
  28. 2
      extensions-api/pom.xml
  29. 36
      extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java
  30. 2
      extensions-core/pom.xml
  31. 2
      extensions/extension-kafka/pom.xml
  32. 2
      extensions/extension-rabbitmq/pom.xml
  33. 2
      extensions/extension-rest-api-call/pom.xml
  34. 2
      extensions/pom.xml
  35. 2
      pom.xml
  36. 2
      tools/pom.xml
  37. 5
      tools/src/main/resources/test.properties
  38. 2
      transport/coap/pom.xml
  39. 5
      transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
  40. 2
      transport/http/pom.xml
  41. 2
      transport/mqtt/pom.xml
  42. 3
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  43. 8
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
  44. 2
      transport/pom.xml
  45. 3
      ui/.eslintrc
  46. 2
      ui/package.json
  47. 2
      ui/pom.xml
  48. 2
      ui/src/app/app.js
  49. 3
      ui/src/app/app.run.js
  50. 40
      ui/src/app/components/dashboard.directive.js
  51. 112
      ui/src/app/components/dashboard.tpl.html
  52. 105
      ui/src/app/components/react/json-form-image.jsx
  53. 79
      ui/src/app/components/react/json-form-image.scss
  54. 2
      ui/src/app/components/react/json-form-schema-form.jsx
  55. 16
      ui/src/app/components/widget.controller.js
  56. 9
      ui/src/app/components/widget.directive.js
  57. 64
      ui/src/app/dashboard/dashboard-settings.controller.js
  58. 91
      ui/src/app/dashboard/dashboard-settings.scss
  59. 115
      ui/src/app/dashboard/dashboard-settings.tpl.html
  60. 29
      ui/src/app/dashboard/dashboard.controller.js
  61. 14
      ui/src/app/dashboard/dashboard.tpl.html
  62. 2
      ui/src/app/dashboard/index.js
  63. 7
      ui/src/app/device/device-fieldset.tpl.html
  64. 13
      ui/src/app/device/device.directive.js
  65. 23
      ui/src/locale/en_US.json

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

2
application/src/main/conf/logback.xml

@ -24,7 +24,7 @@
<file>${pkg.logFolder}/${pkg.name}.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<fileNamePattern>${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>

3
application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.*;
@ -58,6 +59,8 @@ public class DeviceActor extends ContextAwareActor {
processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
} else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) msg);
} else if (msg instanceof DeviceCredentialsUpdateNotificationMsg){
processor.processCredentialsUpdate(context(), (DeviceCredentialsUpdateNotificationMsg) msg);
}
} else if (msg instanceof TimeoutMsg) {
processor.processTimeout(context(), (TimeoutMsg) msg);

26
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -32,13 +32,7 @@ import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
import org.thingsboard.server.common.msg.core.BasicToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.core.SessionCloseMsg;
import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
@ -47,6 +41,7 @@ import org.thingsboard.server.common.msg.session.SessionType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
import org.thingsboard.server.extensions.api.device.DeviceAttributes;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
@ -74,6 +69,7 @@ import java.util.stream.Collectors;
public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
private final DeviceId deviceId;
private final Map<SessionId, SessionInfo> sessions;
private final Map<SessionId, SessionInfo> attributeSubscriptions;
private final Map<SessionId, SessionInfo> rpcSubscriptions;
@ -85,6 +81,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
super(systemContext, logger);
this.deviceId = deviceId;
this.sessions = new HashMap<>();
this.attributeSubscriptions = new HashMap<>();
this.rpcSubscriptions = new HashMap<>();
this.rpcPendingMap = new HashMap<>();
@ -281,7 +278,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
if (!msg.isAdded()) {
logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
.map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
.map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
attributeSubscriptions.entrySet().removeIf(filter);
rpcSubscriptions.entrySet().removeIf(filter);
}
@ -342,8 +339,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private void processSessionStateMsgs(ToDeviceActorMsg msg) {
SessionId sessionId = msg.getSessionId();
FromDeviceMsg inMsg = msg.getPayload();
if (inMsg instanceof SessionCloseMsg) {
if (inMsg instanceof SessionOpenMsg) {
logger.debug("[{}] Processing new session [{}]", deviceId, sessionId);
sessions.put(sessionId, new SessionInfo(SessionType.ASYNC, msg.getServerAddress()));
} else if (inMsg instanceof SessionCloseMsg) {
logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
sessions.remove(sessionId);
attributeSubscriptions.remove(sessionId);
rpcSubscriptions.remove(sessionId);
}
@ -363,4 +364,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return systemContext.getAttributesService().findAll(this.deviceId, attributeType);
}
public void processCredentialsUpdate(ActorContext context, DeviceCredentialsUpdateNotificationMsg msg) {
sessions.forEach((k, v) -> {
sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(new SessionCloseNotification(), k), v.getServer());
});
attributeSubscriptions.clear();
rpcSubscriptions.clear();
}
}

2
application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java

@ -181,7 +181,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
logger.info("[{}] Plugin requires restart due to clazz change from {} to {}.",
entityId, oldPluginMd.getClazz(), pluginMd.getClazz());
requiresRestart = true;
} else if (oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
} else if (!oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
logger.info("[{}] Plugin requires restart due to configuration change from {} to {}.",
entityId, oldPluginMd.getConfiguration(), pluginMd.getConfiguration());
requiresRestart = true;

14
application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java

@ -234,18 +234,18 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd);
try {
fetchPluginInfo();
if (!Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
if (filters == null || !Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
logger.info("[{}] Rule filters require restart due to json change from {} to {}.",
entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters()));
stopFilters();
initFilters();
}
if (!Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
if (processor == null || !Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
logger.info("[{}] Rule processor require restart due to configuration change.", entityId);
stopProcessor();
initProcessor();
}
if (!Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
if (action == null || !Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
logger.info("[{}] Rule action require restart due to configuration change.", entityId);
stopAction();
initAction();
@ -272,13 +272,15 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
if (action != null) {
if (filters != null) {
filters.forEach(f -> f.resume());
} else {
initFilters();
}
if (processor != null) {
processor.resume();
} else {
initProcessor();
}
if (action != null) {
action.resume();
}
action.resume();
logger.info("[{}] Rule resumed.", entityId);
} else {
start();

3
application/src/main/java/org/thingsboard/server/actors/service/ActorService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.actors.service;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
@ -28,4 +29,6 @@ public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor
void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
}

15
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java

@ -32,16 +32,19 @@ import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
import org.thingsboard.server.actors.session.SessionManagerActor;
import org.thingsboard.server.actors.stats.StatsActor;
import org.thingsboard.server.common.data.id.DeviceId;
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.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
@ -56,6 +59,7 @@ import scala.concurrent.duration.Duration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Optional;
@Service
@Slf4j
@ -221,6 +225,17 @@ public class DefaultActorService implements ActorService {
broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
}
@Override
public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) {
DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId);
Optional<ServerAddress> address = actorContext.getRoutingService().resolve(deviceId);
if (address.isPresent()) {
rpcService.tell(address.get(), msg);
} else {
onMsg(msg);
}
}
public void broadcast(ToAllNodesMsg msg) {
rpcService.broadcast(msg);
appActor.tell(msg, ActorRef.noSender());

39
application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java

@ -20,15 +20,14 @@ import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.core.AttributesSubscribeMsg;
import org.thingsboard.server.common.msg.core.ResponseMsg;
import org.thingsboard.server.common.msg.core.RpcSubscribeMsg;
import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.core.SessionCloseMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.session.*;
import akka.actor.ActorContext;
import akka.event.LoggingAdapter;
import org.thingsboard.server.common.msg.session.ctrl.*;
import org.thingsboard.server.common.msg.session.ex.SessionException;
import java.util.HashMap;
@ -37,7 +36,8 @@ import java.util.Optional;
class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
private boolean firstMsg = true;
private Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
private Optional<ServerAddress> currentTargetServer;
private boolean subscribedToAttributeUpdates;
private boolean subscribedToRpcCommands;
@ -49,6 +49,10 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
@Override
protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
updateSessionCtx(msg, SessionType.ASYNC);
if (firstMsg) {
toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
firstMsg = false;
}
ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
switch (fromDeviceMsg.getMsgType()) {
@ -80,17 +84,21 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
@Override
public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
try {
switch (msg.getMsgType()) {
case STATUS_CODE_RESPONSE:
case GET_ATTRIBUTES_RESPONSE:
ResponseMsg responseMsg = (ResponseMsg) msg;
if (responseMsg.getRequestId() >= 0) {
logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
pendingMap.remove(responseMsg.getRequestId());
}
break;
if (msg.getMsgType() != MsgType.SESSION_CLOSE) {
switch (msg.getMsgType()) {
case STATUS_CODE_RESPONSE:
case GET_ATTRIBUTES_RESPONSE:
ResponseMsg responseMsg = (ResponseMsg) msg;
if (responseMsg.getRequestId() >= 0) {
logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
pendingMap.remove(responseMsg.getRequestId());
}
break;
}
sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
} else {
sessionCtx.onMsg(org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg.onCredentialsRevoked(sessionCtx.getSessionId()));
}
sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
} catch (SessionException e) {
logger.warning("Failed to push session response msg", e);
}
@ -102,7 +110,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
}
protected void cleanupSession(ActorContext ctx) {
toDeviceMsg(new SessionCloseMsg()).ifPresent(msg -> forwardToAppActor(ctx, msg));
toDeviceMsg(new SessionCloseMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
}
@Override
@ -110,6 +118,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) {
Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
if (!newTargetServer.equals(currentTargetServer)) {
firstMsg = true;
currentTargetServer = newTargetServer;
pendingMap.values().forEach(v -> {
forwardToAppActor(context, v, currentTargetServer);

2
application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java

@ -52,7 +52,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
if (pendingResponse) {
try {
sessionCtx.onMsg(new SessionCloseMsg(sessionId, true));
sessionCtx.onMsg(SessionCloseMsg.onTimeout(sessionId));
} catch (SessionException e) {
logger.warning("Failed to push session close msg", e);
}

19
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
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;
@RestController
@RequestMapping("/api")
@ -48,7 +49,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device", method = RequestMethod.POST)
@ResponseBody
@ResponseBody
public Device saveDevice(@RequestBody Device device) throws ThingsboardException {
try {
device.setTenantId(getCurrentUser().getTenantId());
@ -74,7 +75,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/device/{deviceId}", method = RequestMethod.POST)
@ResponseBody
@ResponseBody
public Device assignDeviceToCustomer(@PathVariable("customerId") String strCustomerId,
@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
@ -85,7 +86,7 @@ public class DeviceController extends BaseController {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId);
return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
} catch (Exception e) {
throw handleException(e);
@ -94,7 +95,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/device/{deviceId}", method = RequestMethod.DELETE)
@ResponseBody
@ResponseBody
public Device unassignDeviceFromCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
checkParameter("deviceId", strDeviceId);
try {
@ -125,19 +126,21 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
@ResponseBody
@ResponseBody
public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
checkNotNull(deviceCredentials);
try {
checkDeviceId(deviceCredentials.getDeviceId());
return checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
return result;
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/devices", params = { "limit" }, method = RequestMethod.GET)
@RequestMapping(value = "/tenant/devices", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<Device> getTenantDevices(
@RequestParam int limit,
@ -154,7 +157,7 @@ public class DeviceController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/devices", params = { "limit" }, method = RequestMethod.GET)
@RequestMapping(value = "/customer/{customerId}/devices", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<Device> getCustomerDevices(
@PathVariable("customerId") String strCustomerId,

4
application/src/main/resources/thingsboard.yml

@ -138,8 +138,8 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
# Specify max partitions per request
max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:1000}"
# Specify max data points per request
max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:86400}"
# Actor system parameters
actors:

108
application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java

@ -0,0 +1,108 @@
/**
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.mqtt.rpc;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.client.tools.RestClient;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.mqtt.AbstractFeatureIntegrationTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* @author Valerii Sosliuk
*/
@Slf4j
public class MqttServerSideRpcIntegrationTest extends AbstractFeatureIntegrationTest {
private static final String MQTT_URL = "tcp://localhost:1883";
private static final String BASE_URL = "http://localhost:8080";
private static final String USERNAME = "tenant@thingsboard.org";
private static final String PASSWORD = "tenant";
private Device savedDevice;
private String accessToken;
private RestClient restClient;
@Before
public void beforeTest() throws Exception {
restClient = new RestClient(BASE_URL);
restClient.login(USERNAME, PASSWORD);
Device device = new Device();
device.setName("Test Server-Side RPC Device");
savedDevice = restClient.getRestTemplate().postForEntity(BASE_URL + "/api/device", device, Device.class).getBody();
DeviceCredentials deviceCredentials =
restClient.getRestTemplate().getForEntity(BASE_URL + "/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class).getBody();
assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
}
@Test
public void testServerMqttTwoWayRpc() throws Exception {
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
client.connect(options);
Thread.sleep(3000);
client.subscribe("v1/devices/me/rpc/request/+",1);
client.setCallback(new TestMqttCallback(client));
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class).getBody();
log.info("Result: " + result);
Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result);
}
private static class TestMqttCallback implements MqttCallback {
private final MqttAsyncClient client;
TestMqttCallback(MqttAsyncClient client) {
this.client = client;
}
@Override
public void connectionLost(Throwable throwable) {
}
@Override
public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
log.info("Message Arrived: " + mqttMessage.getPayload().toString());
MqttMessage message = new MqttMessage();
String responseTopic = requestTopic.replace("request", "response");
message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes());
client.publish(responseTopic, message);
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
}
}

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

38
common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.msg.core;
import lombok.ToString;
import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
import org.thingsboard.server.common.msg.session.MsgType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
@ToString
public class SessionCloseNotification implements ToDeviceMsg {
private static final long serialVersionUID = 1L;
@Override
public boolean isSuccess() {
return true;
}
@Override
public MsgType getMsgType() {
return MsgType.SESSION_CLOSE;
}
}

29
common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.msg.core;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
import org.thingsboard.server.common.msg.session.MsgType;
/**
* @author Andrew Shvayka
*/
public class SessionOpenMsg implements FromDeviceMsg {
@Override
public MsgType getMsgType() {
return MsgType.SESSION_OPEN;
}
}

2
common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java

@ -28,7 +28,7 @@ public enum MsgType {
RULE_ENGINE_ERROR,
SESSION_CLOSE;
SESSION_OPEN, SESSION_CLOSE;
private final boolean requiresRulesProcessing;

20
common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java

@ -21,11 +21,25 @@ import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
public class SessionCloseMsg implements SessionCtrlMsg {
private final SessionId sessionId;
private final boolean revoked;
private final boolean timeout;
public SessionCloseMsg(SessionId sessionId, boolean timeout) {
public static SessionCloseMsg onError(SessionId sessionId) {
return new SessionCloseMsg(sessionId, false, false);
}
public static SessionCloseMsg onTimeout(SessionId sessionId) {
return new SessionCloseMsg(sessionId, false, true);
}
public static SessionCloseMsg onCredentialsRevoked(SessionId sessionId) {
return new SessionCloseMsg(sessionId, true, false);
}
private SessionCloseMsg(SessionId sessionId, boolean unauthorized, boolean timeout) {
super();
this.sessionId = sessionId;
this.revoked = unauthorized;
this.timeout = timeout;
}
@ -34,6 +48,10 @@ public class SessionCloseMsg implements SessionCtrlMsg {
return sessionId;
}
public boolean isCredentialsRevoked() {
return revoked;
}
public boolean isTimeout() {
return timeout;
}

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

16
dao/src/main/resources/demo-data.cql

@ -287,10 +287,10 @@ VALUES (
'org.thingsboard.server.extensions.core.plugin.mail.MailPlugin',
true,
'{
"host": "smtp.gmail.com",
"port": 587,
"username": "username@gmail.com",
"password": "password",
"host": "smtp.sendgrid.net",
"port": 2525,
"username": "apikey",
"password": "your_api_key",
"otherProperties": [
{
"key":"mail.smtp.auth",
@ -303,14 +303,6 @@ VALUES (
{
"key":"mail.smtp.starttls.enable",
"value":"true"
},
{
"key":"mail.smtp.host",
"value":"smtp.gmail.com"
},
{
"key":"mail.smtp.port",
"value":"587"
}
]
}'

16
dao/src/main/resources/system-data.cql

File diff suppressed because one or more lines are too long

4
docker/docker-compose.yml

@ -18,7 +18,7 @@ version: '2'
services:
thingsboard:
image: "thingsboard/application:1.0"
image: "thingsboard/application:1.0.1"
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.0"
image: "thingsboard/thingsboard-db-schema:1.0.1"
env_file:
- thingsboard-db-schema.env
entrypoint: ./install_schema.sh

4
docker/thingsboard-db-schema/build_and_deploy.sh

@ -20,8 +20,8 @@ cp ../../dao/src/main/resources/schema.cql schema.cql
cp ../../dao/src/main/resources/demo-data.cql demo-data.cql
cp ../../dao/src/main/resources/system-data.cql system-data.cql
docker build -t thingsboard/thingsboard-db-schema:1.0 .
docker build -t thingsboard/thingsboard-db-schema:1.0.1 .
docker login
docker push thingsboard/thingsboard-db-schema:1.0
docker push thingsboard/thingsboard-db-schema:1.0.1

4
docker/thingsboard/build_and_deploy.sh

@ -18,8 +18,8 @@
cp ../../application/target/thingsboard.deb thingsboard.deb
docker build -t thingsboard/application:1.0 .
docker build -t thingsboard/application:1.0.1 .
docker login
docker push thingsboard/application:1.0
docker push thingsboard/application:1.0.1

2
extensions-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

36
extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.api.device;
import lombok.Data;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKey;
import java.util.Set;
/**
* @author Andrew Shvayka
*/
@Data
public class DeviceCredentialsUpdateNotificationMsg implements ToDeviceActorNotificationMsg {
private final TenantId tenantId;
private final DeviceId deviceId;
}

2
extensions-core/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

2
extensions/extension-kafka/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>

2
extensions/extension-rabbitmq/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>

2
extensions/extension-rest-api-call/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>

2
extensions/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

2
pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Thingsboard</name>

2
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

5
tools/src/main/resources/test.properties

@ -0,0 +1,5 @@
restUrl=http://localhost:8080
mqttUrls=tcp://localhost:1883
deviceCount=1
durationMs=60000
iterationIntervalMs=1000

2
transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

5
transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java

@ -36,6 +36,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class CoapSessionCtx extends DeviceAwareSessionContext {
@ -87,6 +88,8 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
private void onSessionClose(SessionCloseMsg msg) {
if (msg.isTimeout()) {
exchange.respond(ResponseCode.SERVICE_UNAVAILABLE);
} else if (msg.isCredentialsRevoked()) {
exchange.respond(ResponseCode.UNAUTHORIZED);
} else {
exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
}
@ -120,7 +123,7 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
public void close() {
log.info("[{}] Closing processing context. Timeout: {}", sessionId, exchange.advanced().isTimedOut());
processor.process(new SessionCloseMsg(sessionId, exchange.advanced().isTimedOut()));
processor.process(exchange.advanced().isTimedOut() ? SessionCloseMsg.onTimeout(sessionId) : SessionCloseMsg.onError(sessionId));
}
@Override

2
transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

3
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -210,7 +210,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
private void processDisconnect(ChannelHandlerContext ctx) {
processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
ctx.close();
}
@ -255,6 +254,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
processor.process(SessionCloseMsg.onError(sessionCtx.getSessionId()));
}
}

8
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java

@ -16,12 +16,13 @@
package org.thingsboard.server.transport.mqtt.session;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.*;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
import org.thingsboard.server.common.msg.session.SessionType;
import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
import org.thingsboard.server.common.msg.session.ex.SessionException;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@ -75,7 +76,10 @@ public class MqttSessionCtx extends DeviceAwareSessionContext {
@Override
public void onMsg(SessionCtrlMsg msg) throws SessionException {
if (msg instanceof SessionCloseMsg) {
pushToNetwork(new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)));
channel.close();
}
}
@Override

2
transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

3
ui/.eslintrc

@ -11,5 +11,8 @@
"node_modules",
"\\.tpl\\.html$"
]
},
"globals": {
"FileReader": true
}
}

2
ui/package.json

@ -14,6 +14,7 @@
"build": "NODE_ENV=production webpack -p"
},
"dependencies": {
"@flowjs/ng-flow": "^2.7.1",
"ace-builds": "^1.2.5",
"angular": "1.5.8",
"angular-animate": "1.5.8",
@ -65,6 +66,7 @@
"react": "^15.4.1",
"react-ace": "^4.1.0",
"react-dom": "^15.4.1",
"react-dropzone": "^3.7.3",
"react-schema-form": "^0.3.1",
"react-tap-event-plugin": "^2.0.1",
"reactcss": "^1.0.9",

2
ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

2
ui/src/app/app.js

@ -42,6 +42,7 @@ import 'react-dom';
import 'material-ui';
import 'react-schema-form';
import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import thingsboardLogin from './login';
import thingsboardDialogs from './components/datakey-config-dialog.controller';
@ -88,6 +89,7 @@ angular.module('thingsboard', [
'angular-carousel',
'ngclipboard',
react.name,
'flow',
thingsboardLogin,
thingsboardDialogs,
thingsboardMenu,

3
ui/src/app/app.run.js

@ -14,9 +14,12 @@
* limitations under the License.
*/
import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min';
/*@ngInject*/
export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $filter, loginService, userService, $translate) {
$window.Flow = Flow;
var frame = $window.frameElement;
var unauthorizedDialog = null;
var forbiddenDialog = null;

40
ui/src/app/components/dashboard.directive.js

@ -51,6 +51,7 @@ function Dashboard() {
widgets: '=',
deviceAliasList: '=',
columns: '=',
margins: '=',
isEdit: '=',
isMobile: '=',
isMobileDisabled: '=?',
@ -61,7 +62,8 @@ function Dashboard() {
onWidgetClicked: '&?',
loadWidgets: '&?',
onInit: '&?',
onInitFailed: '&?'
onInitFailed: '&?',
dashboardStyle: '=?'
},
controller: DashboardController,
controllerAs: 'vm',
@ -108,7 +110,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
},
isMobile: vm.isMobileDisabled ? false : vm.isMobile,
mobileBreakPoint: vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960),
margins: [10, 10],
margins: vm.margins ? vm.margins : [10, 10],
saveGridItemCalculatedHeightInMobile: true
};
@ -159,6 +161,20 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
$scope.$watch('vm.columns', function () {
vm.gridsterOpts.columns = vm.columns ? vm.columns : 24;
if (gridster) {
gridster.columns = vm.columns;
updateGridsterParams();
}
updateVisibleRect();
});
$scope.$watch('vm.margins', function () {
vm.gridsterOpts.margins = vm.margins ? vm.margins : [10, 10];
if (gridster) {
gridster.margins = vm.margins;
updateGridsterParams();
}
updateVisibleRect();
});
$scope.$watch('vm.isEdit', function () {
@ -224,6 +240,26 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}, 0, false);
}
function updateGridsterParams() {
if (gridster) {
if (gridster.colWidth === 'auto') {
gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
} else {
gridster.curColWidth = gridster.colWidth;
}
gridster.curRowHeight = gridster.rowHeight;
if (angular.isString(gridster.rowHeight)) {
if (gridster.rowHeight === 'match') {
gridster.curRowHeight = Math.round(gridster.curColWidth);
} else if (gridster.rowHeight.indexOf('*') !== -1) {
gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
} else if (gridster.rowHeight.indexOf('/') !== -1) {
gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
}
}
}
}
function updateVisibleRect (force, containerResized) {
if (gridster) {
var position = $(gridster.$element).position()

112
ui/src/app/components/dashboard.tpl.html

@ -20,61 +20,63 @@
<md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
</md-content>
<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap>
<div id="gridster-child" gridster="vm.gridsterOpts">
<ul>
<!-- ng-click="widgetClicked($event, widget)" -->
<li gridster-item="widget" ng-repeat="widget in vm.widgets">
<div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
tb-mousedown="vm.widgetMouseDown($event, widget)"
tb-mousemove="vm.widgetMouseMove($event, widget)"
tb-mouseup="vm.widgetMouseUp($event, widget)"
style="
cursor: pointer;
color: {{vm.widgetColor(widget)}};
background-color: {{vm.widgetBackgroundColor(widget)}};
padding: {{vm.widgetPadding(widget)}}
">
<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div>
<div class="tb-widget-actions" layout="row" layout-align="start center">
<md-button id="expand-button"
aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button md-primary"></md-button>
<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
ng-click="vm.editWidget($event, widget)"
aria-label="{{ 'widget.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.edit' | translate }}
</md-tooltip>
<md-icon class="material-icons">
edit
</md-icon>
</md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
ng-click="vm.removeWidget($event, widget)"
aria-label="{{ 'widget.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.remove' | translate }}
</md-tooltip>
<md-icon class="material-icons">
close
</md-icon>
</md-button>
</div>
<div flex layout="column" class="tb-widget-content">
<div flex tb-widget
locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
<div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
<div id="gridster-child" gridster="vm.gridsterOpts">
<ul>
<!-- ng-click="widgetClicked($event, widget)" -->
<li gridster-item="widget" ng-repeat="widget in vm.widgets">
<div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
tb-mousedown="vm.widgetMouseDown($event, widget)"
tb-mousemove="vm.widgetMouseMove($event, widget)"
tb-mouseup="vm.widgetMouseUp($event, widget)"
style="
cursor: pointer;
color: {{vm.widgetColor(widget)}};
background-color: {{vm.widgetBackgroundColor(widget)}};
padding: {{vm.widgetPadding(widget)}}
">
<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div>
</div>
</div>
</li>
</ul>
<div class="tb-widget-actions" layout="row" layout-align="start center">
<md-button id="expand-button"
aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button md-primary"></md-button>
<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
ng-click="vm.editWidget($event, widget)"
aria-label="{{ 'widget.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.edit' | translate }}
</md-tooltip>
<md-icon class="material-icons">
edit
</md-icon>
</md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
ng-click="vm.removeWidget($event, widget)"
aria-label="{{ 'widget.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.remove' | translate }}
</md-tooltip>
<md-icon class="material-icons">
close
</md-icon>
</md-button>
</div>
<div flex layout="column" class="tb-widget-content">
<div flex tb-widget
locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
</md-content>

105
ui/src/app/components/react/json-form-image.jsx

@ -0,0 +1,105 @@
/*
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 './json-form-image.scss';
import React from 'react';
import ThingsboardBaseComponent from './json-form-base-component.jsx';
import Dropzone from 'react-dropzone';
import IconButton from 'material-ui/IconButton';
class ThingsboardImage extends React.Component {
constructor(props) {
super(props);
this.onValueChanged = this.onValueChanged.bind(this);
this.onDrop = this.onDrop.bind(this);
this.onClear = this.onClear.bind(this);
var value = props.value ? props.value + '' : null;
this.state = {
imageUrl: value
};
}
onValueChanged(value) {
this.setState({
imageUrl: value
});
this.props.onChangeValidate({
target: {
value: value
}
});
}
onDrop(files) {
var reader = new FileReader();
reader.onload = (function(tImg) {
return function(event) {
tImg.onValueChanged(event.target.result);
};
})(this);
reader.readAsDataURL(files[0]);
}
onClear(event) {
if (event) {
event.stopPropagation();
}
this.onValueChanged(null);
}
render() {
var labelClass = "tb-label";
if (this.props.form.required) {
labelClass += " tb-required";
}
if (this.props.form.readonly) {
labelClass += " tb-readonly";
}
if (this.state.focused) {
labelClass += " tb-focused";
}
var previewComponent;
if (this.state.imageUrl) {
previewComponent = <img className="tb-image-preview" src={this.state.imageUrl} />;
} else {
previewComponent = <div>No image selected</div>;
}
return (
<div className="tb-container">
<label className={labelClass}>{this.props.form.title}</label>
<div className="tb-image-select-container">
<div className="tb-image-preview-container">{previewComponent}</div>
<div className="tb-image-clear-container">
<IconButton className="tb-image-clear-btn" iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
</div>
<Dropzone className="tb-dropzone"
onDrop={this.onDrop}
multiple={false}
accept="image/*">
<div>Drop an image or click to select a file to upload.</div>
</Dropzone>
</div>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardImage);

79
ui/src/app/components/react/json-form-image.scss

@ -0,0 +1,79 @@
/**
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$previewSize: 100px;
.tb-image-select-container {
position: relative;
height: $previewSize;
width: 100%;
}
.tb-image-preview {
max-width: $previewSize;
max-height: $previewSize;
width: 100%;
height: 100%;
}
.tb-image-preview-container {
position: relative;
width: $previewSize;
height: $previewSize;
margin-right: 12px;
border: solid 1px;
vertical-align: top;
float: left;
div {
width: 100%;
font-size: 18px;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
}
.tb-dropzone {
position: relative;
border: dashed 2px;
height: $previewSize;
vertical-align: top;
padding: 0 8px;
overflow: hidden;
div {
width: 100%;
font-size: 24px;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
}
.tb-image-clear-container {
width: 48px;
height: $previewSize;
position: relative;
float: right;
}
.tb-image-clear-btn {
position: absolute !important;
top: 50%;
transform: translate(0%,-50%) !important;
}

2
ui/src/app/components/react/json-form-schema-form.jsx

@ -26,6 +26,7 @@ import ThingsboardText from './json-form-text.jsx';
import Select from 'react-schema-form/lib/Select';
import Radios from 'react-schema-form/lib/Radios';
import ThingsboardDate from './json-form-date.jsx';
import ThingsboardImage from './json-form-image.jsx';
import ThingsboardCheckbox from './json-form-checkbox.jsx';
import Help from 'react-schema-form/lib/Help';
import ThingsboardFieldSet from './json-form-fieldset.jsx';
@ -45,6 +46,7 @@ class ThingsboardSchemaForm extends React.Component {
'select': Select,
'radios': Radios,
'date': ThingsboardDate,
'image': ThingsboardImage,
'checkbox': ThingsboardCheckbox,
'help': Help,
'array': ThingsboardArray,

16
ui/src/app/components/widget.controller.js

@ -159,6 +159,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
};
vm.gridsterItemInitialized = gridsterItemInitialized;
vm.visibleRectChanged = visibleRectChanged;
function gridsterItemInitialized(item) {
if (item) {
@ -167,6 +168,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
}
}
function visibleRectChanged(newVisibleRect) {
visibleRect = newVisibleRect;
updateVisibility();
}
initWidget();
function initWidget() {
@ -221,11 +227,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.$emit("widgetPositionChanged", widget);
});
$scope.$on('visibleRectChanged', function (event, newVisibleRect) {
visibleRect = newVisibleRect;
updateVisibility();
});
$scope.$on('onWidgetFullscreenChanged', function(event, isWidgetExpanded, fullscreenWidget) {
if (widget === fullscreenWidget) {
onRedraw(0);
@ -318,9 +319,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
function onRedraw(delay, dataUpdate) {
if (!visible) {
//TODO:
/*if (!visible) {
return;
}
}*/
if (angular.isUndefined(delay)) {
delay = 0;
}

9
ui/src/app/components/widget.directive.js

@ -34,12 +34,19 @@ function Widget($controller, $compile, widgetService) {
var widget = locals.widget;
var gridsterItem;
scope.$on('visibleRectChanged', function (event, newVisibleRect) {
locals.visibleRect = newVisibleRect;
if (widgetController) {
widgetController.visibleRectChanged(newVisibleRect);
}
});
scope.$on('gridster-item-initialized', function (event, item) {
gridsterItem = item;
if (widgetController) {
widgetController.gridsterItemInitialized(gridsterItem);
}
})
});
elem.html('<div flex layout="column" layout-align="center center" style="height: 100%;">' +
' <md-progress-circular md-mode="indeterminate" class="md-accent md-hue-2" md-diameter="120"></md-progress-circular>' +

64
ui/src/app/dashboard/dashboard-settings.controller.js

@ -0,0 +1,64 @@
/*
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 './dashboard-settings.scss';
/*@ngInject*/
export default function DashboardSettingsController($scope, $mdDialog, gridSettings) {
var vm = this;
vm.cancel = cancel;
vm.save = save;
vm.imageAdded = imageAdded;
vm.clearImage = clearImage;
vm.gridSettings = gridSettings || {};
vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
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];
function cancel() {
$mdDialog.cancel();
}
function imageAdded($file) {
var reader = new FileReader();
reader.onload = function(event) {
$scope.$apply(function() {
if (event.target.result && event.target.result.startsWith('data:image/')) {
$scope.theForm.$setDirty();
vm.gridSettings.backgroundImageUrl = event.target.result;
}
});
};
reader.readAsDataURL($file.file);
}
function clearImage() {
$scope.theForm.$setDirty();
vm.gridSettings.backgroundImageUrl = null;
}
function save() {
$scope.theForm.$setPristine();
vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
$mdDialog.hide(vm.gridSettings);
}
}

91
ui/src/app/dashboard/dashboard-settings.scss

@ -0,0 +1,91 @@
/**
* Copyright © 2016 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$previewSize: 100px;
.file-input {
display: none;
}
.tb-container {
position: relative;
margin-top: 32px;
padding: 10px 0;
}
.tb-image-select-container {
position: relative;
height: $previewSize;
width: 100%;
}
.tb-image-preview {
max-width: $previewSize;
max-height: $previewSize;
width: auto;
height: auto;
}
.tb-image-preview-container {
position: relative;
width: $previewSize;
height: $previewSize;
margin-right: 12px;
border: solid 1px;
vertical-align: top;
float: left;
div {
width: 100%;
font-size: 18px;
text-align: center;
}
div, .tb-image-preview {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
}
.tb-flow-drop {
position: relative;
border: dashed 2px;
height: $previewSize;
vertical-align: top;
padding: 0 8px;
overflow: hidden;
min-width: 300px;
label {
width: 100%;
font-size: 24px;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
}
.tb-image-clear-container {
width: 48px;
height: $previewSize;
position: relative;
float: right;
}
.tb-image-clear-btn {
position: absolute !important;
top: 50%;
transform: translate(0%,-50%) !important;
}

115
ui/src/app/dashboard/dashboard-settings.tpl.html

@ -0,0 +1,115 @@
<!--
Copyright © 2016 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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.settings' | translate }}">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>dashboard.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>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" 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-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>
</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>
</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>

29
ui/src/app/dashboard/dashboard.controller.js

@ -16,6 +16,7 @@
/* eslint-disable import/no-unresolved, import/default */
import deviceAliasesTemplate from './device-aliases.tpl.html';
import dashboardBackgroundTemplate from './dashboard-settings.tpl.html';
import addWidgetTemplate from './add-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@ -55,6 +56,7 @@ export default function DashboardController(types, widgetService, userService,
vm.onAddWidgetClosed = onAddWidgetClosed;
vm.onEditWidgetClosed = onEditWidgetClosed;
vm.openDeviceAliases = openDeviceAliases;
vm.openDashboardSettings = openDashboardSettings;
vm.removeWidget = removeWidget;
vm.saveDashboard = saveDashboard;
vm.saveWidget = saveWidget;
@ -252,6 +254,24 @@ export default function DashboardController(types, widgetService, userService,
});
}
function openDashboardSettings($event) {
$mdDialog.show({
controller: 'DashboardSettingsController',
controllerAs: 'vm',
templateUrl: dashboardBackgroundTemplate,
locals: {
gridSettings: angular.copy(vm.dashboard.configuration.gridSettings)
},
parent: angular.element($document[0].body),
skipHide: true,
fullscreen: true,
targetEvent: $event
}).then(function (gridSettings) {
vm.dashboard.configuration.gridSettings = gridSettings;
}, function () {
});
}
function editWidget($event, widget) {
$event.stopPropagation();
var newEditingIndex = vm.widgets.indexOf(widget);
@ -368,6 +388,15 @@ export default function DashboardController(types, widgetService, userService,
w.triggerHandler('resize');
}
}).then(function (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;
}
vm.widgets.push(widget);
}, function () {
});

14
ui/src/app/dashboard/dashboard.tpl.html

@ -59,10 +59,22 @@
<md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDeviceAliases($event)">
{{ 'device.aliases' | translate }}
</md-button>
<md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDashboardSettings($event)">
{{ 'dashboard.settings' | translate }}
</md-button>
</section>
<div class="tb-absolute-fill" ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
<div class="tb-absolute-fill"
ng-class="{ 'tb-padded' : !vm.widgetEditMode, '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': '100%',
'background-position': '0% 0%'}"
widgets="vm.widgets"
columns="vm.dashboard.configuration.gridSettings.columns"
margins="vm.dashboard.configuration.gridSettings.margins"
device-alias-list="vm.dashboard.configuration.deviceAliases"
is-edit="vm.isEdit || vm.widgetEditMode"
is-mobile="vm.forceDashboardMobileMode"

2
ui/src/app/dashboard/index.js

@ -34,6 +34,7 @@ import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller';
import DashboardController from './dashboard.controller';
import DeviceAliasesController from './device-aliases.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';
@ -59,6 +60,7 @@ export default angular.module('thingsboard.dashboard', [
.controller('DashboardsController', DashboardsController)
.controller('DashboardController', DashboardController)
.controller('DeviceAliasesController', DeviceAliasesController)
.controller('DashboardSettingsController', DashboardSettingsController)
.controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
.controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
.controller('AddWidgetController', AddWidgetController)

7
ui/src/app/device/device-fieldset.tpl.html

@ -36,6 +36,13 @@
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>device.copyId</span>
</md-button>
<md-button ngclipboard data-clipboard-action="copy"
ngclipboard-success="onAccessTokenCopied(e)"
data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"
class="md-raised">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>device.copyAccessToken</span>
</md-button>
</div>
<md-content class="md-padding" layout="column">

13
ui/src/app/device/device.directive.js

@ -20,18 +20,23 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function DeviceDirective($compile, $templateCache, toast, $translate, types, customerService) {
export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) {
var linker = function (scope, element) {
var template = $templateCache.get(deviceFieldsetTemplate);
element.html(template);
scope.isAssignedToCustomer = false;
scope.assignedCustomer = null;
scope.deviceCredentials = null;
scope.$watch('device', function(newVal) {
if (newVal) {
deviceService.getDeviceCredentials(scope.device.id.id).then(
function success(credentials) {
scope.deviceCredentials = credentials;
}
);
if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true;
customerService.getCustomer(scope.device.customerId.id).then(
@ -50,6 +55,10 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
scope.onAccessTokenCopied = function() {
toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
$compile(element.contents())(scope);
}
return {

23
ui/src/locale/en_US.json

@ -192,7 +192,26 @@
"select-existing": "Select existing dashboard",
"create-new": "Create new dashboard",
"new-dashboard-title": "New dashboard title",
"open-dashboard": "Open dashboard"
"open-dashboard": "Open dashboard",
"set-background": "Set background",
"background-color": "Background color",
"background-image": "Background image",
"no-image": "No image selected",
"drop-image": "Drop an image or click to select a file to upload.",
"settings": "Settings",
"columns-count": "Columns count",
"columns-count-required": "Columns count is required.",
"min-columns-count-message": "Only 10 minimum column count is allowed.",
"max-columns-count-message": "Only 1000 maximum column count is allowed.",
"widgets-margins": "Margin between widgets",
"horizontal-margin": "Horizontal margin",
"horizontal-margin-required": "Horizontal margin value is required.",
"min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.",
"max-horizontal-margin-message": "Only 50 is allowed as maximum horizontal margin value.",
"vertical-margin": "Vertical margin",
"vertical-margin-required": "Vertical margin value is required.",
"min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
"max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value."
},
"datakey": {
"settings": "Settings",
@ -280,7 +299,9 @@
"events": "Events",
"details": "Details",
"copyId": "Copy device Id",
"copyAccessToken": "Copy access token",
"idCopiedMessage": "Device Id has been copied to clipboard",
"accessTokenCopiedMessage": "Device access token has been copied to clipboard",
"assignedToCustomer": "Assigned to customer",
"unable-delete-device-alias-title": "Unable to delete device alias",
"unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}"

Loading…
Cancel
Save